赞
踩
头文件:#include<sys/types.h>
#include<sys/socket.h>
函数名:socket
函数功能:创建一个通信端口
参数1:int domain :使用的协议族
参数2:type :套接字的类型
参数3:protocol :默认为0
返回值:int 成功-> 创建的新的套接字的文件描述符,失败->-1和错误代码
int socket(int domain , int type , int protocol);
是通信协议族,使用Lunix命令【man socket】可以查看他的手册:
为了实现稳定的TCP通信,传入AF_INET,标识使用IPv4网络协议。
SOCK_STREAM:流式套接字,TCP使用
SOCK_DGRAM:数据报套接字,UDP使用
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:bind 函数功能:给socket绑定IP和端口(需要被找到的套接字才需要被绑定) 参数1:int sockfd:创建出来的新的套接字的文件描述符 参数2:const struct sockaddr *addr:存储自己的IP地址、端口的结构的结构体首地址 参数3:socklen_t addrlen : addrlen结构体的大小 返回值:int 成功->返回0,失败->-1和错误代码 int bind(int sockfd , const struct sockaddr *addr , socklen_t addrlen); struct sockaddr { sa_family_t sa_family; //地址族 char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息 };
需要注意的是:bind主要用于服务器端,客户端创建的套接字可以不绑定地址
TCP搭建服务器使用的地址结构并不是struct sockaddr , 而是struct sockaddr_in
struct sockaddr_in
{
short int sin_family; //网络协议
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //32位IP地址
};
sockaddr_in结构体内还包含一个结构体:struct in_addr
struct in_addr
{
unit32_t s_addr; //32位IP地址
};
listen本质上是一个监听队列,在accept接收客户端连接之前,对将要链接的套接字进行标记。如果监听队列已经满了,再有新的客户端发起连接请求时,则无法监听,此时客户端可能会收到连接被拒绝的错误。
服务器端成功建立套接字并与地址进行绑定后,调用listen函数,将套接字标记为被动(监听模式)。准备接收客户端的连接请求
头文件:#include<sys/types.h>
#include<sys/socket.h>
函数名:listen
参数1:int sockfd:监听套接字文件描述符(即socket创建,被bind绑定的套接字)
参数2:int backlog:监听队列的大小
返回值:int 成功返回0, 失败返回-1和错误代码
int listen(int sockfd , int backlog);
头文件:#include<sys/types.h>
#include<sys/socket.h>
函数名:accpet
函数功能:接收一个套接字的连接请求
参数1:int sockfd :监听套接字的文件描述符(经过listen的sockfd)
参数2:struct sockaddr *addr :用来存储对方(客户端)地址结构的内存首地址
【对方的IP地址和端口号】
参数3:socklen_t *addrlen:存储地址结构信息长度变量的地址
返回值:成功->通信套接字的文件描述符,失败返回-1和错误代码
int accept(int sockfd , struct sockaddr *addr , socklen_t *addrlen);
accept接受连接的方式有两种:
1:不关心客户端的IP地址和端口号时,参数2和参数3设置为NULL
2:若要读取并保存客户端的IP地址和端口号,则需要新建一个socket_in结构体
做完前四步后就可以进行通信了。TCP/IP提供了一种通信方式:send函数和recv函数
头文件:#include<sys/types.h>
#include<sys/socket.h>
函数名:send
函数功能:通过socket发送数据
参数1:int sockfd:通信套接字的文件描述符
参数2:const void *buf :被发送的数据的首地址
参数3: sizeof_t len:想要发送的字节数(数据长度)
参数4:int flags:默认为0
返回值:ssize_t 类型。成功->返回成功发送的字节数,失败->返回-和错误代码
ssize_t send(int sockfd , const void *buf , size_t len , int flags);
头文件:#include<sys/types.h>
#include<sys/socket.h>
函数名:recv
函数功能:通过socket接收数据
参数1:int sockfd :通信套接字的文件描述符
参数2:const void *buf:用来存储接收的数据的内存首地址
参数3:size_t len :想要接收的字节数(数据长度)
参数4:int flags:默认为0
返回值:ssize_t类型;成功->返回成功接收道德字节数,失败->返回-1
和错误码;0:表示对端执行了一个有序关闭。
通信结束,关闭连接
如果malloc申请了空间,则需要free释放
如果打开了文件,则需要关闭fd
最后close(套接字)
客户端的搭建较为简单,只需四个步骤:socket-connect-send/recv-close
头文件:#include <sys/types.h>
#include <sys/socket.h>
函数功能:发起一个socket的连接请求
参数1:int sockfd:客户端socket函数创建的套接字的文件描述符
参数2:const struct sockaddr *addr: 存储服务器的IP地址和端口结构的内存首地址
参数3:socklen_t addrlen:addr只想的内存空间大小
返回值:int类型:成功->返回0,失败->返回-1和错误码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
介绍完客户-服务器的搭建流程,开始写代码
//服务器端模块化 //编写一子函数,实现监听客户端连接服务器 //参数1:IP地址 char * ip //参数2:端口号 int port //返回值:成功->返回监听的socket对象,失败返回-1 int tcp_server(char * ip , int port); //函数实现 nt tcp_server(char * ip , int port) //子函数实现 { //1.创建socket对象TCP //2.绑定自己的IP地址&端口号 //3.监听是否有人链接 //1.创建socket对象TCP int tcp_fd = -1; // tcp_fd = socket(AF_INET , SOCK_STREAM , 0); //1.1判断是否绑定成功 if(0 > tcp_fd) { perror("socket error!"); return -1; } puts("socket success!"); //2.绑定自己的IP地址&端口号 //结构体见顶部注释 struct sockaddr_in myaddr; //定义一个结构体myaddr,数据类型为struct sockaddr_in myaddr.sin_family = AF_INET; //协议 myaddr.sin_port = htons(port); //端口号,atoi()意思是将主机字节序转换为网络字节序 myaddr.sin_addr.s_addr = inet_addr(ip);//IP地址 //2.2bind if(bind(tcp_fd , (struct sockaddr *)&myaddr , sizeof(myaddr)) < 0) { perror("bind error!"); //打印错误原因 close(tcp_fd); //关闭 return -1; } puts("bind success!"); //3.监听是否有人链接 if(0 > listen(tcp_fd , 5)) //参数2:监听队列的大小,一般设置为5 { perror("listen error!"); close(tcp_fd); return -1; } puts("listen success!"); return tcp_fd; //监听成功,返回socket对象tcp_fd }
接下来时accept和通信
//编写一子函数,和客户端进行通信 //参数:accept后新生成的socket对象 //返回值:void int tcp_server_communiction(int acp_fd); //函数实现 int tcp_server_communiction(int acp_fd) { //5.1:收 //5.1.1:入参判断 if(0 > acp_fd) { perror("This acp_fd is NULL!"); return -1; } //5.1.2: acp_fd的格式正确,首先接受client客户端发送的数据 char buf[1024]; //定义一个数组,存储收发的文件 memset(buf , 0 , sizeof(buf)); //清空数组 //read(lst_fd , buf , sizeof(buf)); //将客户端发送的数据通过IO文件操作中的read方式读入到该数组中 //换第二种文件操作方式: send和recv,这是TCP/IP自带的一种文件收发方式 int rec = -1; //定义一个变量rec用于接收recv()函数的返回值 rec = recv(acp_fd , buf , sizeof(buf) , 0);//执行接收函数 //5.1.3:判断是否接收成功 if(0 > rec) { perror("recv error!"); return -1; } //接收正常,但还要判断是否为空数据 if(0 < buf) { //证明数组中接收到了客户端发来的数据 puts("buf"); char ncmd[10] = {0}; //用来存放客户端的操作方式:是上传[up]还是下载[down] char filename[64] = {0}; //用于存放客户端要操作的对象名字 //还需要定义一个子函数,以解析客户端发来的命令 analysis_cmd(buf , ncmd , filename); //调用命令解析函数,将客户端发送的命令和文件名解析出来,并且存储到前面定义的ncmd[]和filename[]数组中 printf("%s\n" , ncmd); printf("%s\n" , filename); //成功拿到命令和文件名 //下载文件 if(0 == strcmp(ncmd , "down")) //调用字符串比较函数,两个字符串相同则返回0 { //下载文件 int fd = open(filename , O_RDONLY);//fn:文件名,O_RDONLY:以只读的方式打开文件 //判断文件是否打开成功 if(0 > fd) { //fd为open函数的返回值,小于0表明文件打开失败 char msg[30] = "文件不存在"; //调用send函数,将这个字符串发送给client客户端 send(acp_fd , msg , sizeof(msg) , 0); return -1; } else { //文件打开成功 int len = lseek(fd , 0 , SEEK_END); printf("要发送的文件长度为:%d\n" , len); char fl[30] = {0}; sprintf(fl , "lend:%d" , len);//将计算到的文件长度输出重定向到fl[]数组中 //将文件长度先返回给客户端,以便于客户端创建一个同等大小的数组用于接收文件 send(acp_fd , fl , sizeof(fl) , 0);//发送 //然后准备发送文件 //由于lseek已经指向文件末尾了,所以需要让他回到文件开头 lseek(fd , 0 , SEEK_SET); //SEEK_SET:文件的起始位置 /*大文件需要在栈区申请空间*/ char *picture = NULL; //创建一个图片指针 picture = (char *)malloc(len); //为这个指针开辟一片内存空间 //判断是否开辟成功 if(NULL == picture) { perror("malloc error!"); return -1; } //开辟成功 //清空 memset(picture , 0 , sizeof(picture)); int red = read(fd , picture , len); //判断是否读取文件成功 if(len == red) //read()函数,读取文件成功时,会返回文件长度 { //将读取到的文件数据发送给client客户端 send(acp_fd , picture , sizeof(picture) , 0); } sleep(5); free(picture); close(fd); } } else if(0 == strcmp(ncmd , "up")) { //上传文件 //首先创建一个数组用来接收客户端发来的文件大小信息 char msg[30] = {0}; int ret = -1; ret = recv(acp_fd , msg , sizeof(msg) , 0); //接收客户端的文件信息 //判断 if(0 < ret) { puts(msg); //客户发来的数组fl已经接收成功,打印出来 //解析字符串中的数字 } if(strstr(msg , "lenth")) //通过字符串定位函数,定位到数字的位置 { int len = atoi(msg + 6);//提取字符串中的数字 //printf("文件大小为:%d\n" , len); int fd = -1; // //调用open函数创建一个文件,大小为提取出的len fd = open(filename , O_WRONLY | O_CREAT , 0777); if(0 > fd) { puts("open | creat file error!"); close(fd); return -1; } //接收文件,大文件需要存放在堆区,使用maloc申请,使用完需手动释放 char *file = NULL; file = (char *)malloc(len); if(NULL == file) { puts("malloc error"); close(fd); return -1; } //大文件无法一次接收完,需要循环接受 int t = 0; int rec = -1; while(t < len) { memset(file , 0 , len); rec = recv(acp_fd , file , len-t , 0); if(0 < rec) { write(fd , file , rec); t += rec; } sleep(1); } puts("文件接收成功!"); free(file); close(fd); } } } //5.2:发 /*memset(buf , 0 , sizeof(buf)); //先清空存储数据的数组 printf("write:"); fgets(buf , sizeof(buf) , stdin); //标准输入流:键盘获取数据到存储数据的数组中 write(acp_fd , buf , strlen(buf)); close(acp_fd); */ }
最后是字符串分析函数,通过此函数可以将客户端发来的【命令 文件】分离出来
臂如:down 1.jpg 说明客户端要从服务器下载一张名为1.jpg的图片
//定义一个子函数,解析从客户端接受到的命令 //从该命令(str字符串)中,解析出操作方式up 或 down // 解析出要操作的对象 filename int analysis_cmd(char *str , char *cmd , char *filename); //参数1:从客户端接收到的字符串 //参数2:用于存储解析出的命令 //参数3:用于存储解析出的文件名 //返回值:无 //函数实现 int analysis_cmd(char *str , char *cmd , char *filename) //解析命令 { //1.入参判断 if(NULL == str || NULL == cmd || NULL == filename) { perror("参数不匹配,请重新输入!"); return -1; } char *p = str; //定义一个指针p指向接收的字符串str的首地址 while(*p) { if(' ' == *p) //命令与文件名之间有一个空格 { break; //跳出循环 } else { *cmd = *p; //从str的起始位置开始,将p指向的字符依次复制cmd cmd += 1; //cmd是一个存储命令的数组,数组名就是数组的首地址,所以cmd+1,表示指针向后移动一个位置 } //跳出while循环时表明已经将命令遍历出来了[解析出操作方式:up或down] p += 1; //p指针向后移动 } strcpy(filename , p+1); //这里的p还指在命令与文件名间的空格处,所以需要再向后移动一个位置才是文件名的首地址 //使用strcpy函数将p指针之后的字符复制给存储filename的数组 }
最后是主函数,调用
int main(int argc , char *argv[]) { //0.判断main函数传参是否正确 if(2 > argc) { perror("请输入正确的参数:IP地址 端口号"); return -1; } //1.监听socket对象 int tcp_fd = -1; tcp_fd = tcp_server(argv[1] , atoi(argv[2])); while(1) //accept,用于将客户端的链接请求与服务器建立TCP通信,并返回一个新的已经建立链接的accept套接字 { //2.接受链接 //若不关心客户端的IP地址和端口号,则将参数2,3设置为空 //4.1:定义一个保存客户端IP地址和端口号的结构体并清空 int lst_fd = -1; struct sockaddr_in client; //创建一个结构体 memset(&client , 0 , sizeof(client)); //4.2:定义一个int型变量,保存结构体的大小 int len = sizeof(client); //4.3:接受连接,并且保存客户端的信息 lst_fd = accept(tcp_fd , (struct sockaddr *)&client , &len); //4.4:将网络字节序ip地址转换为主机字节序ip地址 char * ip = inet_ntoa(client.sin_addr); //4.5:网络字节序port转换为主机字节序port unsigned short port = ntohs(client.sin_port); //4.6:打印出客户端的ip地址和端口号 printf("client IP = %s , PORT = %d \n" , ip , port); //lst_fd = accept(tcp_fd , NULL , NULL); //不需要获取客户端的ip地址和端口号 //4.7判断是否链接成功 if(0 > lst_fd) { perror("accept error!"); close(tcp_fd); return -1; } puts("accept success!"); //do_work() //2.和客户端进行通信 tcp_server_communiction(lst_fd); //6.关闭socket对象 close(tcp_fd); return 0; } }
socket+connect,返回connect成功的套接字
//定义一个tcp连接的子函数 //参数1:ip地址 char * //参数2:端口号 int port //返回值:成功->返回链接好的socket对象,失败返回-1 int tcp_connect(char * ip , int port); //函数实现 int tcp_connect(char * ip , int port) //子函数实现 { //1.创建TCP socket //2.设置对方的IP和端口号 //3.请求链接 //1.创建TCP socket int tcp_fd = -1; //初始化为-1 tcp_fd = socket(AF_INET , SOCK_STREAM , 0); //IPV4,流式套接字 //1.1 判断是否创建成功 if(0 > tcp_fd) { perror("socket error!"); return -1; } puts("socket success!"); //2.设置对方的IP和端口号[bind] //2.1 定义一个结构体变量 struct sockaddr_in myser; //类似于c++的实例化对象 // myser.sin_family = AF_INET; //协议 myser.sin_addr.s_addr = inet_addr(ip); myser.sin_port = htons(port); //主机字节序转网络字节序 //3.请求链接 int ret = -1; ret = connect(tcp_fd , (struct sockaddr *)&myser , sizeof(myser)); //connect连接 if(0 != ret) { perror("connect error!"); close(tcp_fd); return -1; } puts("connect success!"); return tcp_fd; //返回connect成功后新生成的套接字 }
和服务器进行通信
//定义一个子函数,和服务器端进行通信 //参数:创建好的socket对象 //返回值:void int tcp_client_communiction(int tcp_fd); // //函数实现 /*do_work*/ int tcp_client_communiction(int tcp_fd) //子函数:和服务器进行通信的实现 { //入参判断 if(0 > tcp_fd) { printf("%s < 0 , connot to communictioning with server!\n" , tcp_fd); return -1; } //4.发送消息:给server发送下载或上传指令 char buf[50]; memset(buf , 0 , sizeof(buf)); gets(buf); //输入 send(tcp_fd , buf , strlen(buf) , 0); char ncmd[10] = {0}; char filename[64] = {0}; analysis_cmd(buf , ncmd , filename); //puts(ncmd); //puts(filename); //sleep(2); if(0 == strcmp(ncmd , "down")) { //send后准备recv,接受服务器的文件 //定义一个数组,用来接受server发过来的文件长度message char msg[30] = {0}; int ret = -1; ret = recv(tcp_fd , msg , sizeof(msg) , 0); puts(msg); if(0 < ret) { //接收命令成功 puts(buf); //打印buf里接收到的文本字符串[待接收的文件大小] //判断是否是server发来的错误信息 if(0 == strcmp(msg , "文件不存在")) { return 0; } else if(strstr(msg , "len")) { int len = atoi(msg + 5); //提取字符串中的数字 printf("文件长度为:%d\n" , len); //用open函数新建一个文件,大小为len int fd = -1; fd = open(filename , O_WRONLY | O_CREAT , 0777); //判断文件描述符是否创建成功 if(0 > fd) { puts("open|creat file error!"); close(fd); return -1; } //接收文件 char *file = NULL; file = (char *)malloc(len); //判断文件接收空间是否申请成功 if(NULL == file) { puts("malloc error!"); close(fd); return -1; } int rec = -1; //一次接收不完,需要循环接收 int t = 0; while(t < len) { memset(file , 0 , len); rec = recv(tcp_fd , file , len-t , 0); //recv的成功的返回值是接受到文件的大小 //判断是否接受成功 //printf("%d\n" , rec); if(0 < rec) { //puts("进来了"); write(fd , file , rec); t += rec; } //printf("%d\n",t); sleep(1); } puts("文件接收成功!"); free(file); close(fd); } } else if(0 == ret) //没有接受到任何字符 { return 0; } } else if(0 == strcmp(ncmd , "up")) { //上传文件 //文件操作,打开文件 int fd = open(filename , O_RDONLY); //以只读的方式打开文件 //judge if(0 > fd) { //由于是客户向服务器上传文件,所以不必告诉服务器文件是否上传错误,只需告诉客户端就行 puts("文件打开失败,请重新上传!"); close(fd); return -1; } //打开成功,需要计算文件长度并发送给服务器 int lenth = -1; lenth = lseek(fd , 0 , SEEK_END); //调用lseek函数,从0位置跳到文件末尾,计算出文件长度 printf("主人,您要上传的文件大小为:%d\n" , lenth); char fl[30] = {0}; sprintf(fl , "lenth:%d" , lenth); //将字符串输出重定向到数组中,发送该数组给服务器 send(tcp_fd , fl , sizeof(fl) , 0); //准备发送文件 lseek(fd , 0 , SEEK_SET); //回到文件起始位置 char *file = NULL; file = (char *)malloc(lenth); //申请空间,将图片存放于此,等待发送 if(NULL == file) { perror("malloc error!"); return -1; } memset(file , 0 , lenth); int red = (fd , file , lenth); if(red == lenth) { //读取成功,准备向服务器发送 send(tcp_fd , file , lenth , 0); } sleep(5); free(file); close(fd); } return 0; }
同样,客户端也需要一个字符串解析函数,所以直接拷贝服务器里面的函数
//字符串解析函数,解析出文件大小 int analysis_cmd(char *str , char *cmd , char *filename) //解析命令 { //1.入参判断 if(NULL == str || NULL == cmd || NULL == filename) { perror("参数不匹配,请重新输入!"); return -1; } char *p = str; //定义一个指针p指向接收的字符串str的首地址 while(*p) { if(' ' == *p) //命令与文件名之间有一个空格 { break; //跳出循环 } else { *cmd = *p; //从str的起始位置开始,将p指向的字符依次复制cmd cmd += 1; //cmd是一个存储命令的数组,数组名就是数组的首地址,所以cmd+1,表示指针向后移动一个位置 } //跳出while循环时表明已经将命令遍历出来了[解析出操作方式:up或down] p += 1; //p指针向后移动 } strcpy(filename , p+1); //这里的p还指在命令与文件名间的空格处,所以需要再向后移动一个位置才是文件名的首地址 //使用strcpy函数将p指针之后的字符复制给存储filename的数组 }
最后是main函数的调用:
int main(int argc , char *argv[]) { //0.首先判断传参是否格式正确 if(2 > argc) { printf("传入参数不够,请重新传参!"); return -1; } //1.连接服务器 int tcp_fd = -1; tcp_fd = tcp_connect(argv[1] , atoi(argv[2])); //atoi()将传入的端口号[例如:8888]转化为网络字节序 //do_work //2.和服务器端进行通信 tcp_client_communiction(tcp_fd); //3.关闭socket对象 close(tcp_fd); return 0; }
【net.h】
#ifndef _NET_H
#define _NET_H
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<fcntl.h>
#endif
代码中用到了一些字符串处理函数:例如strstr()、atoi()、
以及网络IP地址转换函数:htonl()或是htons()
char *strstr(const *str1 , const char *str2)
//从str1中寻找字符串str2第一次出现的位置
结果就是:程序会从第一个xiao处打印后面的字符串
//atoi()
#include<stdio.h>
#include<stdlib.h>
int main()
{
char arr1[] = "-1234";
char arr2[] = "len:569456";
printf("%d\n" , atoi(arr1));
printf("%d\n" , atoi(arr2+4));//提取字符串中的数字
return 0;
}
在不同的文件夹中分别运行客户服务器端的程序,设定IP地址为本地回环地址:127.0.0.1
端口号:8888
分别在服务器目录下存放一张照片,在客户端目录下存放一个.txt文档,然后测试。
首先是客户端需要将服务器的图片下载至自己的目录下,然后再从自己目录上传一个文档至服务器。
结束!
一点点感悟,网编流程基本不变,服务器与客户端的搭建模式只要记清,剩下的就是文件操作了,只是需要考虑的一些细节很多,稍不注意可能会有Bug,没错,我的程序也有Bug,文件下载完成后客户端会进入最后的while死循环出不来,目前还没检查出来问题所在。TCP/IP这一块确实重要,需要认真学习。我自己也做了一点点笔记,如有需要程序源码和思维导图,请留言邮箱,我必回复。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。