TCP是一种传输层的网络协议,是一种面向连接的,可靠的,基于字节流的网络协议。进行TCP通信的时候,双方一定要先建立连接,也就是我们所说的三次握手,建立稳定连接之后,接下来就是我们的通信了,接下来就可以进行正常的发送和收发数据。收发数据的时候是基于字节流的。
TCP编程模型
模型解析:
1.首先,服务器端先用户区创建一个socket文件,随后绑定内核空间中一个网卡设备的映射,(为何需要绑定呢,因为我们创建一个socket的时候只是创建了而已,并没有绑定到网卡设备),绑定的时候会进行ip地址和端口号的设置,接下来就监听相应的ip地址和端口号。accept会阻塞等待客户端的连接。
2.客户端会创建一个socket文件并且通过connect去连接服务器。
3.以上两点完成后,客户端和服务器端就创建好连接了,接下来就可以正常收发数据了
4.收发完数据之后,要结束连接,结束连接就要进行四次挥手。
我们在写TCP代码之前先了解一下socket文件,我们之前学io编程的时候都知道,在Linux当中一切皆文件,文件类型我们分成三种,一种是字符型文件,第二种是块类型文件,第三者网络型设备。对于字符型文件,比如磁盘,我们会在内核空间映射出磁盘的一个file,并且在用户空间通过文件描述符去操控这个file,并且各个file是相互独立的。但是网卡设备略有不同,同样的,我们在用户空间有socket描述符,在内核空间有网卡设备的相应驱动,我们要自己绑定相应的网卡设备地址,唯一不同的是,我们绑定一块网卡地址好像可以同时操控网络上的所有网卡,比如客户端创建了一个socket号,这个socket号不是单单对客户端网卡设备有效,对其他网卡一样有效,所以我们就可以通过相同的socket号同时从客户端和服务器端两个网卡中进行通信。是跨网卡的。说白了客户端和服务器端组成了一个超级网卡,两端可以同时向这个超级网卡写数据,读数据,并且两端同时操作这个超级网卡的时候不会冲突,cpu会采用分时复用进行避免冲突
服务端代码
//服务器端
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd;//服务端的socket号
int cnt;
char redBuff[128];//读取缓冲区
int connect_fd;//客户端的socket号
int ret_bind;//绑定函数返回值
struct sockaddr_in serve_addr;//服务器的网卡设备地址
socklen_t serve_len,client_len;
//1.创建服务器端socket号
sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.配置服务器端网卡设备相关参数
serve_addr.sin_family = AF_INET;//协议族:IPV4
serve_addr.sin_port = 8090;//端口号
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//将ip地址由主机字节序转化为网络字节序
serve_len = sizeof(serve_addr);//服务器端地址长度
client_len = sizeof(client_addr);//客户端地址长度
//3.socket号绑定网卡设备地址
ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//4.监听连接请求
listen(sockfd,3);
//5.等待客户端的连接
while(1)
{
connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
if(connect_fd < 0)
{
perror("connect faile\n");
exit(1);
}
//6.建立连接,进行通信
while(1)
{
cnt = read(connect_fd,redBuff,128);//通过客户端socket号进行读取数据
if(cnt < 0)
{
perror("read fail\n");
exit(1);
}
else if(cnt == 0)
{
printf("client offline....\n");
break;
}else
{
printf("server:%s\n",redBuff);
write(connect_fd,"I receive",10);//通过客户端socket号进行发数据
}
}
}
return 0;
客户端代码
//客户端代码
#include
#include
#include
#include
#include
#include
#include
int main()
{
char redBuff[128];//读取缓冲区
int write_ret,read_ret;
int client_fd,server_fd,ret;
struct sockaddr_in serve_addr;//服务端的网卡设备地址
//1.创建客户端socket号
client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置服务端网卡设备地址参数
serve_addr.sin_family = AF_INET;//IPV4协议
serve_addr.sin_port = 8090;//服务端端口号
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务端的ip地址
//3.申请连接
ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
if(ret == -1)
{
perror("connect failed\n");
exit(1);
}
//4.收发数据
write_ret = write(client_fd,"helloworld\n",16);//通过客户端socket号发送数据给服务端
if(write_ret < 0)
{
perror("write failed\n");
exit(1);
}
printf("client write end\n");
read_ret = read(client_fd,redBuff,128);//通过客户端socket号接收服务端数据
if(read_ret < 0)
{
perror("read fail\n");
exit(2);
}
printf("client:%s\n",redBuff);
//5.结束通信
close(client_fd);
return 0;
}
三次握手是TCP通信中建立连接环节
1.首先客户端通过connect函数向服务端发送一个请求通信的syn包,并且进入SYN_SEND状态
2.服务端收到这个syn包,并且确认这个syn包,随后服务端通过accept函数再发送一个syn包给客户端表示确认请求
3.客户端再发送一个包给服务端,再次确认
通信图
我们建立连接,通完信后,我们要将连接关掉,即关闭相应的socket值,调用close函数,如果不关闭socket,内核会一直维护这个socket。我们关闭连接的时候需要进行四次挥手动作。
四次挥手图
详解:
1.客户端应用进程调用close函数,TCP会发送一个FIN M包,告知服务器说我要关闭连接了
2.服务器收到FIN M包后确认一下这个包,执行被动关闭,随后发一个ACK M+1包给客户端表示我已经收到了,随后就会关闭。
3.过一会,服务器主动调用close函数,同时发一个FIN N包告知客户端,说我要关闭了。
4.客户端收到FIN N包后,发一个ACK N+1包告知服务器说我已经收到你的关闭信息了
服务器代码
//服务器端代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (4*1024)
/*--------------------
该函数具有以下功能:
1.处理客户端发送来的请求信息
2.形参1:cfd,是客户端的socket号
3.形参2: cmd,是客户端发送来的请求信息
-----------------------*/
void cmd_parse(int cfd,char *cmd)
{
int video_fd;
int video_readRet;
char redBuff[RSIZE] = {0};//读取缓冲区
//1.容错检测,如果是错误信息,则返回
if( cmd == NULL)
{
printf("nothing\n");
return;
}
//2.如果命令是getText,即获取文本
if(strcmp(cmd,"getText") == 0)
{
printf("get text\n");
write(cfd,"get text",20);
}
//3.如果命令是getPhoto,即获取照片
else if(strcmp(cmd,"getPhoto") == 0)
{
printf("getPhoto\n");
write(cfd,"getPhoto",20);
}
//4.如果命令是获取getVideo,即获取视频
else if(strcmp(cmd,"getVideo") == 0)
{
video_fd = open("../mp4/a.mp4",O_RDONLY);//打开一个文件视频
if(video_fd < 0) return;
while(1)
{
video_readRet = read(video_fd,redBuff,RSIZE);
printf("Ret:%d\n",video_readRet);
write(cfd,redBuff,video_readRet);
if(video_readRet < RSIZE) break;
}
close(video_fd);
}
else
{
write(cfd,"i do not understand",22);
}
}
int main()
{
int sockfd;//服务器端socket号
int cnt;
struct sockaddr_in client_addr = {0};//客户端的网卡设备地址
char redBuff[128];//读取缓冲区
int connect_fd;//客户端socket号
int ret_bind;//连接函数的返回值,用以检测
struct sockaddr_in serve_addr;//服务器的网卡设备地址
socklen_t serve_len,client_len;
//1.创建服务器端的socket号
sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置服务器端的网卡设备相关参数
serve_addr.sin_family = AF_INET;//IPV4的协议
serve_addr.sin_port = 8090;//端口号
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//将协议族的主机字节序转化为网络字节序
serve_len = sizeof(serve_addr);
client_len = sizeof(client_addr);
//3.将socket号绑定服务器端的网卡设备地址
ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//4.监听客户端请求
listen(sockfd,3);
//5.等待客户端连接
while(1)
{
connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
if(connect_fd < 0)
{
perror("connect faile\n");
exit(1);
}
//通信
while(1)
{
cnt = read(connect_fd,redBuff,128);
if(cnt < 0)
{
perror("read fail\n");
exit(1);
}
else if(cnt == 0)
{
printf("client offline....\n");
break;
}else
{
//请求处理函数
cmd_parse(connect_fd,redBuff);
}
}
}
close(sockfd);
close(connect_fd);
return 0;
客户端代码
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (4*1024)
int main()
{
char redBuff[RSIZE];//读取缓冲区
int video_readRet;
char choose[20] ={0};
int write_ret,read_ret;
int client_fd,server_fd,ret;
struct sockaddr_in client_addr;//客户端网卡设备地址
int video_fd;
struct sockaddr_in serve_addr;//服务器端网卡设备地址
//1.创建客户端socket号
client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置客户端的网卡设备地址相关参数
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");
//3.申请连接服务端
do
{
ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
}while(ret != 0);
while(1)
{
printf("enter the choose[1-16]\n");
scanf("%s",choose);
video_fd = open("../mp4/c.mp4",O_WRONLY|O_CREAT,0777);
write_ret = write(client_fd,choose,20);
if(video_fd < 0)
{
perror("open video failed\n");
return -1;
}
while(1)
{
video_readRet = read(client_fd,redBuff,RSIZE);
write(video_fd,redBuff,video_readRet);
printf("ret:%d\n",video_readRet);
if(video_readRet < RSIZE) break;
}
close(video_fd);
sleep(2);
system("clear\n");
}
close(client_fd);
return 0;
我们知道TCP是面向连接,可靠的,基于数据流的一种传输层协议,所以我们在利用TCP进行通信的时候,数据都是以流的形式进行发送,各个数据之间没有明确的边界之分,比如我们有如下开发要求:客户端向服务器端发送请求,要求服务器将其目录下的所有图片发送过来。网卡是存在缓冲区的,即数据会先暂存网卡缓冲区,等待网卡缓冲区满了再发送出去,这样就会有一个情况发生,比如发送a和b两张图片,先发送a,当a的最后一些字节数据被送进缓冲区之后缓冲区还没满,这时由于cpu执行速度很快,所以b的数据就很快被送进网卡缓冲区,这样a和b的数据就被发送出去了,客户端就无法辨别a和b了,这就是粘包问题。
解决方案有:延时、定长包、加边界符、非定长的数据包,其中非定长的数据包就是这个包含有这个包的长度和包的名称,先发送包头告知客户端要发生的文件名以及文件的大小,客户端接收到这个包就知道要发送的文件大小,然后按照指定大小进行读取,这样就不会粘包了。
服务端代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (128)//读取缓冲区
struct package
{
char name[48];//文件名字
int size;//文件的大小
};
/*----------------------
该函数具有以下功能:
1.处理客户端请求
2.封包,发包
-----------------------*/
void cmd_parse(int cfd,char *cmd)
{
DIR* dir;
int video_fd;
struct dirent* drt;
char redBuff[RSIZE] = {0};//读取缓冲区
struct package pag;
struct stat st;
int read_ret,write_ret;
char tempfile[48] = {0};
if( cmd == NULL)
{
printf("nothing\n");
// return;
}
if(strcmp(cmd,"getText") == 0)
{
printf("get text\n");
write(cfd,"get text",20);
}
else if(strcmp(cmd,"getPhoto") == 0)
{
printf("getPhoto\n");
write(cfd,"getPhoto",20);
}
else if(strcmp(cmd,"getVideo") == 0)
{
//1.打开图片目录
dir = opendir("../jpg");
if(dir == NULL) return;
//2.遍历图片目录
while((drt = readdir(dir)) != NULL)
{
if((strcmp(drt->d_name,".") == 0 || (strcmp(drt->d_name,"..") == 0))) continue;
//2.打包完,并发送包
strcpy(pag.name,drt->d_name);//初始化包的名称
sprintf(tempfile,"%s/%s","../jpg",drt->d_name);//字符串拼接,用以打开文件和获取文件大小
if(stat(tempfile,&st) != 0)
{
perror("stat failed\n");
exit(1);
}
pag.size = st.st_size;//初始化包的大小
write(cfd,&pag,sizeof(pag));//发送包
//3.打开文件并发送文件
video_fd = open(tempfile,O_RDONLY);
if(video_fd < 0)
{
perror("open tempfile failed\n");
exit(1);
}
while((read_ret = read(video_fd,redBuff,RSIZE)) > 0)
{
write_ret = write(cfd,redBuff,read_ret);
printf("read_ret:%d write_ret:%d\n",read_ret,write_ret);
}
close(video_fd);
}
//4.发送一个结束包给客户端
strcpy(pag.name,"over");
pag.size = 0;
write(cfd,&pag,sizeof(pag));
}
else
{
write(cfd,"i do not understand",22);
}
}
int main()
{
int sockfd;
int cnt;
struct sockaddr_in client_addr = {0};
char redBuff[128];
int connect_fd;
int ret_bind;
struct sockaddr_in serve_addr;
socklen_t serve_len,client_len;
//1.创建服务器socket号
sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.配置服务器网卡地址的相关参数
serve_addr.sin_family = AF_INET;//IPV4协议族
serve_addr.sin_port = 8090;//端口号
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//主机字节序转化为网络字节序
serve_len = sizeof(serve_addr);//服务端地址长度
client_len = sizeof(client_addr);//客户端地址长度
//3.将socket号将网卡地址绑定
ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//4.监听客户端连接请求
listen(sockfd,3);
//5.等待客户端连接
while(1)
{
连接客户端,并返回客户端的connect_fd号
connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
if(connect_fd < 0)
{
perror("connect faile\n");
exit(1);
}
//收发数据
while(1)
{
cnt = read(connect_fd,redBuff,128);
if(cnt < 0)
{
perror("read fail\n");
exit(1);
}
else if(cnt == 0)
{
printf("client offline....\n");
break;
}else
{
//处理接收到的命令
cmd_parse(connect_fd,redBuff);
}
}
}
close(sockfd);
close(connect_fd);
return 0;
}
客户端代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (128)
struct package
{
char name[48];//文件的名称
int size;//文件的大小
};
int main()
{
char redBuff[RSIZE];//读取缓冲区
int video_readRet;
char choose[20] ={0};//命令缓冲区
int write_ret,read_ret;
int client_fd,server_fd,ret;
struct sockaddr_in client_addr;
int video_fd;
struct sockaddr_in serve_addr;
struct package pag;
char tempfile[48] = {0};
//1.创建客户端socket号
client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置网卡设备地址
serve_addr.sin_family = AF_INET;//IPV4协议
serve_addr.sin_port = 8090;//端口号与服务器相同
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务器端ip地址
//3.连接
do
{
ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));//连接服务端
}while(ret != 0);
while(1)
{
//4.输入命令和发送命令
printf("enter the choose[1-16]\n");
scanf("%s",choose);
write_ret = write(client_fd,choose,20);
while(1)
{
//1.获得数据包
read(client_fd,&pag,sizeof(pag));
//2.先判断是不是结束包,是的话直接结束
if(pag.size == 0 && strcmp(pag.name,"over") == 0) break;
//3.文件读写操作
sprintf(tempfile,"%s/%s","../cpjpg",pag.name);
video_fd = open(tempfile,O_RDWR|O_CREAT|O_TRUNC,0777);
if(video_fd < 0)
{
perror("open failed\n");
exit(2);
}
while( pag.size > 0)
{
read_ret = read(client_fd,redBuff,RSIZE);
write(video_fd,redBuff,read_ret);
pag.size = pag.size-read_ret;
}
close( video_fd);
}
sleep(2);
system("clear\n");
}
close(client_fd);
return 0;
}
并发服务器即同一个时刻可以响应多个客户端的请求,上面建立的服务器和客户端都是一对一的,只能一个客户端连接。常用的并发服务技术有:多进程并发服务器、多线程并发服务器、IO复用并发服务器。这里讲述的是多进程并发服务器
就是将建立连接和收发信息分开来做,主进程一直监听是否有客户端进行申请连接,如果有,马上通过fork函数建立一个子进程,该子进程会复制父进程的本身的socket号,并且拿到申请连接客户端的socket号,通过客户端的socket号完成与新客户端完成通信,子进程完成通信后,父进程要完成子进程的资源回收,避免资源的浪费。资源回收利用信号通信,所有的子进程在进程结束后都会触发一个SIGCHLD这个信号,只要在父进程改造这个信号,当收到这个信号后马上回收资源即可。
服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#include
//信号改造函数
void sig_handle(int signum)
{
wait(NULL);
}
int main()
{
//1.改造信号
if(signal(SIGCHLD,sig_handle) == SIG_ERR)
{
perror("signal failed\n");
return -1;
}
char redBuff[128];//读取缓冲区
int sockfd;//服务器socket号
int connect_fd;//客户端socket号
int cnt;
pid_t pid;//子进程pid
struct sockaddr_in serve_addr;//服务器网卡地址
struct sockaddr_in client_addr;//客户端网卡地址
int ret_bind;//bind函数的返回值
socklen_t serve_len,client_len;//服务器和客户端的地址长度
//2.创建服务器端socket
sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
perror("sock failed\n");
exit(1);
}
//3.设置服务器网卡设备参数
serve_addr.sin_family = AF_INET;//IPV4协议族
serve_addr.sin_port = 8090;//端口号
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//主机字节序转化为网络字节序
serve_len = sizeof(serve_addr);//地址长度
client_len = sizeof(client_addr);
//4.将网卡设备和服务器socket号绑定
ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//5.监听端口号和ip
listen(sockfd,3);
//6.建立连接和通信
while(1)
{
//这个循环可以保证主线程一直阻塞在这里,除非有新客户端申请连接
connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
if(connect_fd < 0)
{
perror("connect faile\n");
exit(1);
}
//创建子进程
pid = fork();
if(pid < 0)
{
perror("fork failed\n");
exit(1);
}
if(pid == 0)
{
//子进程负责通信
close(sockfd);//子线程用不到服务器socket号,只需要客户端socket号,关掉即可。
//通信
printf("child pid = %u\n",(unsigned int)getpid());
while(1)
{
cnt = read(connect_fd,redBuff,128);
if(cnt < 0)
{
perror("read fail\n");
exit(1);
}
else if(cnt == 0)
{
printf("client offline....\n");
return 0;
}else
{
printf("server:%s\n",redBuff);
}
}
close(connect_fd);
}
else
{
close(connect_fd);//服务端不需要利用客户端socket号进行通信,故直接关掉即可
}
}
close(sockfd);
return 0;
}
客户端
#include
#include
#include
#include
#include
#include
#include
int main()
{
int cnt = 50;
char redBuff[128];//读取缓冲区
int write_ret,read_ret;//write函数和read函数的返回值
int client_fd,ret;//客户端的socket号
struct sockaddr_in serve_addr;//服务器端网卡设备地址
//1.创建客户端socket号
client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置服务器端网卡设备地址参数
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务器端ip地址
//3.申请连接
ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
if(ret == -1)
{
perror("connect failed\n");
exit(1);
}
//通信
write_ret = write(client_fd,"helloworld\n",16);
if(write_ret < 0)
{
perror("write failed\n");
exit(1);
}
while(cnt--)
{
printf("client pid :%u\n",(unsigned int)getpid());
sleep(1);
}
close(client_fd);
return 0;
}
我们在多进程并发服务器当中,每个连接的数据收发都要创建一个新的子进程用来通信,这样对资源的消耗十分大。所以引入io设备的复用,接下来我们介绍一下io设备。
io设备就是具有输入输出功能的设备,在Linux中,这些设备全被抽象成文件操作符,即通过编写程序操作文件描述符就可以完成io设备的操作,写操作用write函数,读操作用read函数,这些io设备受io控制器的控制,cpu不能直接操作io设备,需要io控制器这个中介,io控制器解析cpu指令,然后去操作io设备。
IO设备通常只有一个,如果存在多个外部主机模块与之进行输入输出通信,则此io设备会出现并发竟态,需要分时复用
在Linux中,我们可以使用select函数进行io复用,比如,当有多个客户端与服务器连接,那么多个客户端想要与服务器端通信,只能通过服务器端一个网卡,只要网卡工作的够快,那么就可以在发现有数据包过来的情况下及时转移缓冲区数据,实现并发通信。只需要以下三个步骤:
1.及时发现交互数据包
2.查出要与之通信的客户端
3.快速实现收发动作
select函数就可以完成监听工作。
1.头文件
2.函数原型
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)
①参数1-nfds:需要检查的号码最高的文件描述符加1(关键不能错)
②参数2-readfds:这是一个读集合,这个集合里面都是文件描述符,select函数会监听这些描述符,看他们的状态是否可读。
③参数3-writefds:这是一个写集合,这个集合里面都是文件描述符,select函数会监听这些描述符,看他们的状态是否可写。
④参数4-exceptfds:这是一个错误集合,这个集合里面都是文件描述符,select函数会监听这些描述符,看他们的状态是否有错误。
⑤参数5-timeout:这是一个结构体,表示监听的时间是多久,过了这个时间就会返回0
具体解释select的参数:
(1)intmaxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。
说明:对于这个原理的解释可以看上边fd_set的详细解释,fd_set是以位图的形式来存储这些文件描述符。maxfdp也就是定义了位图中有效的位的个数。
(2)fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
(3)fd_set*writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
(4)fd_set*errorfds同上面两个参数的意图,用来监视文件错误异常文件。
(5)structtimeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
说明:
函数返回:
(1)当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回一个大于0的数。
(2)当没有满足条件的文件描述符,且设置的timeval监控时间超时时,select函数会返回一个为0的值。
(3)当select返回负值时,发生错误。
3.函数返回值
①错误时:返回-1
②正确时:返回能读或者能写的文件描述符总数
③超时:返回0
4.结构体struct timeval
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}
有三种情况:
①timeout == NULL 等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。
②timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。
③timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。
5.fd_set集合
fd_set是一个二进制数组。如下图:
对于 fd_set类型的变量我们所能做的就是声明一个变量,为变量赋一个同种类型变量的值,或者使用以下几个宏来控制它:
int FD_ZERO(int fd, fd_set *fdset);//将该集合全部清空
int FD_CLR(int fd, fd_set *fdset);//将fd从fdset集合中清除掉
int FD_SET(int fd, fd_set *fd_set);//将fd加入到fdset集合中去
int FD_ISSET(int fd, fd_set *fdset);//判断fd是否被标记
6.select模型理解
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
7.关于select的几点说明
①使用select函数查询时,一旦fd_set集合中有文件描述符是可读的,那么就会将这个描述符标记,如果文件描述符缓冲区的东西没有被读走,那么下次查询这个文化描述符的时候依然是可读的。我们写个测试案例:
#include
#include
#include
#include
#include
int main()
{
fd_set rset;
int select_ret;
FD_ZERO(&rset);
while(1)
{
FD_SET(0,&rset);//将标准输入文件描述符加入到rset中去
select_ret = select(1,&rset,NULL,NULL,NULL);
if(select_ret > 0)
{
if(FD_ISSET(0,&rset))//如果标准输入可读
{
printf("ok\n");
FD_CLR(0,&rset);
sleep(1);
}
}
}
return 0;
}
测试结果:
我键盘输入”input“,然后程序就会一直打印ok,即使我使用FD_CLR这个函数将0从rset集合中删除,依然会打印ok。
结果分析:说明如果0那个标记没有被清除,每次select对rset查验的时候总是会返回一个正值。那么如何清除这个标记呢
解决办法:将缓冲区的东西读走,那么标准输入就变成不可读了,即标记清除(其他设备也是一样,将缓冲区内容读走)
#include
#include
#include
#include
#include
int main()
{
fd_set rset;
char redBuff[16];//读取缓冲区
int select_ret;
FD_ZERO(&rset);
while(1)
{
FD_SET(0,&rset);//将标准输入文件描述符加入到rset中去
select_ret = select(1,&rset,NULL,NULL,NULL);
if(select_ret > 0)
{
if(FD_ISSET(0,&rset))//如果标准输入可读
{
printf("ok\n");
scanf("%s",redBuff);
sleep(1);
}
}
}
return 0;
}
测试结果:
我输入input,就打印一次ok,因为输入缓冲区的内容被读走了,标准输入0就不是可读的了,所以就不会打印ok
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int select_ret,max_fd;//select函数返回值和文件描述集合中最大的文件描述符
fd_set rset,temp_set;//两个读集合
int cdf;
struct timeval time;//select函数的参数4
int sockfd;//服务器端的socket号
int cnt;
struct sockaddr_in client_addr = {0};//客户端网卡设备地址
char redBuff[128];//读取缓冲区
int connect_fd;//客户端socket号
int ret_bind;//bind函数的返回值
struct sockaddr_in serve_addr;//服务器端的网卡设备地址
socklen_t serve_len,client_len;//服务器和客户端网卡地址长度
//1.创建服务器端socket号
sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置服务器端网卡设备地址参数
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
serve_len = sizeof(serve_addr);
client_len = sizeof(client_addr);
//3.将服务器端socket号和服务器网卡设备地址绑定
ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//4.监听
listen(sockfd,3);
max_fd = sockfd;//最大文件描述符为sockfd
//5.等待连接
FD_ZERO(&rset);
FD_ZERO(&temp_set);
FD_SET(sockfd,&rset);//将服务器的sockfd加入到rset集合中去
while(1)
{
//初始化timeval
time.tv_sec = 2;
time.tv_usec = 1000;
temp_set = rset;//目的在于将temp_set传给select
select_ret = select(max_fd+1,&temp_set,NULL,NULL,&time);
if(select_ret == 0) printf("time out\n");
else if(select_ret < 0) printf("erro\n");
else
{
//查询到sockfd可读,就是有申请连接
if(FD_ISSET(sockfd,&temp_set))
{
connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);//拿到客户端socket号
if(connect_fd < 0)
{
perror("connect faile\n");
exit(1);
}
FD_SET(connect_fd,&rset);//加入到rset,让select去查询客户端是否有发送数据
max_fd = max_fd>connect_fd ? max_fd:connect_fd;//修改最大的fd号
}
//循环判断连接的客户端是否有信息发过来,即检查客户端socket号是否可读
else
{
for(cdf = 4; cdf < max_fd+1; cdf++)
{
//通信
if(FD_ISSET(cdf,&rset))
{
cnt = read(cdf,redBuff,128);
if(cnt < 0)
{
perror("read fail\n");
exit(1);
}
else if(cnt == 0)
{
//客户端退出
printf("client offline....\n");
FD_CLR(cdf,&rset);//将该文件描述符删除掉,让select不再查询这个文件描述符。
break;
}else
{
printf("cdf = %d %s\n",cdf,redBuff);
sleep(1);
}
}
}
}
}
}
close(sockfd);
close(connect_fd);
return 0;
}
这里不用将sockfd从rset删除,为什么呢,因为第一次我们查询sockfd发现有客户端申请连接,那么就说明网卡缓冲区有内容,然后我们通过accept把缓冲区内容读走之后,sockfd他就变得不可读了,所以就不会重复进来这个分支结构。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (4*1024)
int main()
{
char redBuff[RSIZE];
int video_readRet;
int cnt = 20;
char choose[20] ={0};
int write_ret,read_ret;
int client_fd,server_fd,ret;
struct sockaddr_in client_addr;
int video_fd;
struct sockaddr_in serve_addr;
//1.create the socket
client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.set the serve addr
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");
//3.connect
do
{
ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
}while(ret != 0);
while(cnt--)
{
printf("cnt = %d\n",cnt);
write(client_fd,"client",7);
sleep(1);
}
close(client_fd);
while(1);
return 0;
}
~
在第九节中,我们使用select函数来构造io复用并发服务器。但是select有以下缺点:
①高并发场景下耗费资源大:
select的运行过程是,首先你在用户空间要创建一个fd_set集合,该集合是文件描述符集合,也是个二进制数组,随后调用select函数后,内核会拷贝一份一模一样的fd_set集合,并且先在内核里面循环遍历,然后将结果个数再返回到用户空间。拷贝的时候开辟空间消耗大,每次select都要拷贝
②遍历查询的时候是个同步过程:
调用select函数的时候,用户进程是会堵塞的,会等待内核遍历出结果后才会不堵塞,这是个同步过程,所以效率不高。
③select仅仅返回可遍历的个数:
具体哪个文件描述符可以读是需要用户自己遍历的。
基于以上三个缺点,epoll的引出就很有必要。
10.2epoll的优点
第一步:调用int epoll_create(int size)函数,该函数成功的话会返回一个epoll句柄,size告诉内核此次监听描述符的数目。在内核空间会创建一个eventpoll结构体,里面会有rdlist(检查就绪队列),rb(红黑树),wq(阻塞等待队列)。
第二步:调用int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);并且启动后台服务线程,监听查询红黑树,如果有数据到达网卡,到达epitem,服务线程查询红黑树,若符合,epitem会将fd送到就绪队列,并且唤醒进程,epoll_wait拿走就绪队列的fd的值
struct epoll_event {
uint32_t events; // epoll 事件类型,包括可读,可写等
epoll_data_t data; // 用户数据,可以是一个指针或文件描述符等
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
个人猜测:
①struct epoll_event结构体就是红黑树单节点,将这个单节点的fd设置为一个新的fd,并且插入到红黑树当中去,epoll_ctl函数有插入红黑树节点的功能,跟我们之前写的插入节点函数差不多
②内核里面有个线程一直查询这个红黑树,有新的fd申请连接了,内核会将那个新的fd跟红黑树上的fd进行比较,如果符合,就传给epoll_wait函数
****以上纯属个人猜测****
第二步操作就是比select函数好的优点:
①每次调用epoll_ctl函数,我们只需要添加一个fd进入红黑树即可,不需要将全部需要监听的fd拷入内核,这大大降低资源消耗和时间消耗。
②一旦创建了红黑树,内核就会启动一个内核服务线程去监听这个红黑树,符合条件的马上放到就绪队列当中去,然后通知进程取走数据。注意是通知,通知完就继续查询。
③红黑树遍历效率高
第三步:调用int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
events这个结构体数组有以下特点:
每次监听完将符合要求的文件描述符放到数组的时候,每次返回都会覆盖一遍数组内容,比如一开始符合的是fd = 1和fd = 2,那么events[0].fd = 1和events[1].fd = 2,下次若fd = 4符合,那么events[0].fd = 4, events[1].fd = 2,第二个元素依然为2,因为没有覆盖完。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_SIZE 10000
int main()
{
int i;
int create_fd;
int current;
int epollWait_ret;
struct epoll_event ev;
struct epoll_event events[20];//文件描述符接收数组
int cdf;
int sockfd;
int cnt;
struct sockaddr_in client_addr = {0};
char redBuff[128];
int connect_fd;
int ret_bind;
struct sockaddr_in serve_addr;
socklen_t serve_len,client_len;
//1.创建服务器socket号
sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置服务器网卡设备地址参数
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
serve_len = sizeof(serve_addr);
client_len = sizeof(client_addr);
//3.绑定网卡和socket号
ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//4.监听
listen(sockfd,3);
//5.创建epoll
create_fd = epoll_create(MAX_SIZE);
if(create_fd < 0)
{
perror("epoll_create failed\n");
exit(1);
}
ev.events = EPOLLIN|EPOLLET;//事件设置为可读和边沿触发
ev.data.fd = sockfd;//监听的描述符
epoll_ctl(create_fd,EPOLL_CTL_ADD,sockfd,&ev);//插入和监听描述符
current = 1;//此时监听数目(要实时改变)
//5.等待客户端连接
while(1)
{
epollWait_ret = epoll_wait(create_fd,events,current,1000);
if(epollWait_ret < 0)
{
printf("epoll_wait failed\n");
}
else if(epollWait_ret == 0)
{
printf("time out\n");
}
else
{
//遍历返回的数组
for(i = 0; i
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (4*1024)
int main()
{
char redBuff[RSIZE];
int video_readRet;
int cnt = 20;
char choose[20] ={0};
int write_ret,read_ret;
int client_fd,server_fd,ret;
struct sockaddr_in client_addr;
int video_fd;
struct sockaddr_in serve_addr;
//1.create the socket
client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.set the serve addr
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");
//3.connect
do
{
ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
}while(ret != 0);
while(cnt--)
{
printf("cnt = %d\n",cnt);
write(client_fd,"client",7);
sleep(1);
}
close(client_fd);
return 0;
}
~
UDP报文格式:包含源端口,目的端口号, 用户包长度
UDP相对于TCP的优点:
①传输数据快:
前提是为建立连接的TCP,如果建立连接的TCP那一定是TCP通信快。因为UDP不需要建立连接,直接就可以收发数据。
②传输数据有边界:
因为UDP是传输数据包的,所以每个包之间有边界,但是TCP则不同,TCP是以流的形式传输,故TCP会出现粘包的问题。
③可以一对多广播通讯:
UDP的一对多是同时的,TCP的并发服务器的一对多只是分时复用的。
UDP相对于TCP的缺点:
①不可靠,不能保证有序传输:
TCP中含有传输控制(滑动机制和重发机制),重发机制会校验数据包,如果丢包了是需要对方重新发送包的,但是UDP丢包就丢包了,也不管。
UDP和TCP的选择:
①如果需要间歇工作:UDP或者TCP短链接
②如果需要长期工作: TCP
通信模型:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_SIZE 128
int main()
{
int cdf;
int listen_fd;
int cnt;
struct sockaddr_in client_addr = {0};
char redBuff[MAX_SIZE];
int ret_bind;
struct sockaddr_in serve_addr;
socklen_t serve_len,client_len;
//1.创建服务器端socket
listen_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(listen_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.设置服务器网卡设备地址参数
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
serve_len = sizeof(serve_addr);
client_len = sizeof(client_addr);
//3.将socket号跟网卡绑定
ret_bind = bind(listen_fd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
exit(2);
}
//5.等待客户端连接
while(1)
{
cnt = recvfrom(listen_fd,redBuff,MAX_SIZE,0,(struct sockaddr *)&client_addr,&client_len);
if(cnt < 0) printf("revfrom data faile\n");
printf("redBuff:%s\n",redBuff);
sendto(listen_fd,"get it",7,0,(struct sockaddr *)&client_addr,client_len);
}
close(listen_fd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_SIZE 128
int main()
{
char redBuff[MAX_SIZE];
int client_fd;
struct sockaddr_in client_addr;
struct sockaddr_in serve_addr;
socklen_t server_len,client_len;
//1.create the socket
client_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(client_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//2.set the serve addr
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;//服务器端口号
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务器ip地址
server_len = sizeof(serve_addr);
//3.connect
sendto(client_fd,"hello",6,0,(struct sockaddr*)&serve_addr,server_len);
printf("send finished\n");
recvfrom(client_fd,redBuff,MAX_SIZE,0,NULL,NULL);
printf("data:%s\n",redBuff);
close(client_fd);
return 0;
}
代码几点说明:
①服务器端bind绑定的是自己的端口号8090,不是客户端的端口号,一般来说bind绑定的都是自己的,而客户端的端口号是随机的
②客户端设置的网卡设备地址参数都是服务器的ip和端口号
1.UDP广播通信对UDP基础通信有以下改进:
①发送端将发送的数据包包装成广播包发送出去,发送端当成客户端
②接收端充当服务器,一直接收数据
2.如何封装成广播包:
①将广播包的ip地址设置为广播地址192.168.1.255
②设置好接收方的端口号
发送端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_SIZE 128
int main()
{
char redBuff[MAX_SIZE];
int send_fd;
int on = 1;
struct sockaddr_in client_addr;
struct sockaddr_in serve_addr;
socklen_t server_len,client_len;
//1.创建发送端socket
send_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(send_fd < 0)
{
perror("sock failed\n");
exit(1);
}
//设置成广播模式
setsockopt(send_fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
//2.设置网卡设备地址(广播包设置)
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 8090;//只有端口号为8090的才能接收
serve_addr.sin_addr.s_addr = inet_addr("192.168.1.255");//ip地址为广播地址
server_len = sizeof(serve_addr);
//3.connect
sendto(send_fd,"hello",6,0,(struct sockaddr*)&serve_addr,server_len);
serve_addr.sin_port = 9000;//再发送一个包给端口号为9000的接收方。
sendto(client_fd,"hello",6,0,(struct sockaddr*)&serve_addr,server_len);
recvfrom(client_fd,redBuff,MAX_SIZE,0,NULL,NULL);
printf("send finished\n");
printf("data:%s\n",redBuff);
close(client_fd);
return 0;
}
接收端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_SIZE 128
int main()
{
int cdf;
int listen_fd;
int cnt;
struct sockaddr_in client_addr = {0};
char redBuff[MAX_SIZE];
int ret_bind;
int on =1; struct sockaddr_in serve_addr;
socklen_t serve_len,client_len;
//1.create the socket
listen_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(listen_fd < 0)
{
perror("sock failed\n");
exit(1);
}
setsockopt(listen_fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
//2.set the serve addr
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = 9000;
serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
serve_len = sizeof(serve_addr);
client_len = sizeof(client_addr);
//3.绑定自身的网卡设备地址
ret_bind = bind(listen_fd,(struct sockaddr*)&serve_addr,serve_len);
if(ret_bind < 0)
{
perror("bind failed\n");
//5.wait the client to connect
}
while(1)
{
sendto(listen_fd,"get it",7,0,(struct sockaddr *)&client_addr,client_len);
printf("rec finished\n");
cnt = recvfrom(listen_fd,redBuff,MAX_SIZE,0,(struct sockaddr *)&client_addr,&client_len);
if(cnt < 0) printf("revfrom data faile\n");
printf("redBuff:%s\n",redBuff);
}
close(listen_fd);
return 0;
}