大家好,我是练习编程时长两年半的个人练习生昆工第一ikun,今天我们来分享一下如何让TCP通信可以像UDP那样做到多个客户端可以同时与服务器进行通信,我们可以使用非阻塞模式和多进程多线程,但是这两种方法有其弊端,下面我们将介绍一种弊端较少的方法——IO多路复用。
目录
- 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
- 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间
- 若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;
比较好的方法是使用I/O多路复用。其基本思想是︰
- 先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
- 函数返回时告诉进程那个描述符已就绪,可以进行I/0操作。
一、select机制
1.selec的相关函数
大部分Unix/Linux都支持select函数,该函数用于探测多个文件句柄的状态变
化。
#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);功能:select 机制参数说明:nfds: 最大文件描述符+1readfds: 读事件的表writefds: 写事件的表exceptfds: 异常事件的表timeout:超时检测NULL: 一直阻塞,直到文件描述符准备好为止返回值:成功: 准备好的文件描述符的个数失败: -1
void FD_CLR(int fd, fd_set *set); // 把表中删除一个文件描述符int FD_ISSET(int fd, fd_set *set); //检测文件描述符是否准备好了,准备好了返回1,否则返回0void FD_SET(int fd, fd_set *set); // 加入到表中void FD_ZERO(fd_set *set); // 清空表
基本原理:首先创建一张文件描述符表(fd_set),通过使用特有的函数(select),让内核帮
助上层用户循环检测是否有可操作的文件描述符,如果有则告诉应用程序去操作。
2.select机制实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int tcpser_init(int port);
int main(int argc, const char *argv[])
{
int sockfd = tcpser_init(11111);
printf("wait a client.............\n");
fd_set readfds, tmpfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
char buf[64];
int i;
int maxfd = sockfd;
tmpfds = readfds;
while(1)
{
readfds = tmpfds;
int ret = select(maxfd+1, &readfds, NULL, NULL, NULL);
if(ret == -1)
{
perror("select");
return -1;
}
for(i = sockfd; i < maxfd+1; i++)
{
if(FD_ISSET(i, &readfds))
{
if(i == sockfd)
{
int connfd = accept(sockfd, NULL, NULL);
if(connfd < 0)
{
perror("accept");
return -1;
}
printf("%d is link\n", connfd);
FD_SET(connfd, &tmpfds);
if(maxfd < connfd)
{
maxfd = connfd;
}
else
{
memset(buf, 0, 64);
ret = recv(i, buf, sizeof(buf), 0);
if(ret < 0)
{
close(i);
perror("recv");
return -1;
}
else if(ret == 0) //客户端关闭了
{
printf("%d is unlink\n", i);
FD_CLR(i, &tmpfds);
close(i);
break;
}
else
{
printf("message:%s\n", buf);
}
}
}
}
}
}
close(sockfd);
return 0;
}
int tcpser_init(int port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("sockfd");
return -1;
}
printf("sockfd = %d\n", sockfd);
//端口复用函数:解决端口号被系统占用的情况
int on = 1;
int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(k == -1)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in serveraddr,clientaddr;
memset(&serveraddr, 0 ,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址
int len = sizeof(serveraddr);
int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
if(ret == -1)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 100);
if(ret == -1)
{
perror("listen");
return -1;
}
return sockfd;
}
3.select机制的缺点
select缺点:(1)内核使用轮询的方式来检查文件描述符集合中的描述符是否就绪,文件描 述符越多,消耗的时间资源越多。(2)文件描述符集合使用的数组,有大小限制,1024(3)每次文件描述符集合更新时,重新拷贝到内核中
二、poll机制
1.poll机制的相关函数
poll的实现和select非常相似,只是文件描述符fd集合的方式不同, poll使用struct pollfd结构而不是select的fd_set结构,其他的都差不多。
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);功能:poll机制参数:fds: 结构体数组的数组名struct pollfd{int fd; /* file descriptor */short events; /* requested events */short revents; /* returned events */};fd: 文件描述符events: 请求的事件(读、写、异常)revents: 对请求事件的反馈 (读、写、异常)POLLIN :读事件POLLOUT:写事件eg: struct pollfd fds[];fds[0].fd;fds[0].events;fds[o].revents;nfds: 准备的文件描述符的个数timeout: -1 阻塞,直到文件描述符准备好返回值:成功:准备好的文件描述符的个数失败:-1
2.poll机制的实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <poll.h>
int tcpser_init(int port);
int main(int argc, const char *argv[])
{
int sockfd = tcpser_init(11111);
printf("wait a client.............\n");
struct pollfd fds[100];
memset(&fds, 0, sizeof(fds));
fds[0].fd = sockfd;
fds[0].events = POLLIN;
int m = 1;
int n = 1;
int i, ret;
char buf[64] = {0};
while(1) //轮巡检测结构体数组;
{
ret = poll(fds, m, -1);
if(ret == -1)
{
perror("poll");
exit(-1);
}
for(i = 0; i < m; i++)
{
if(fds[i].events & POLLIN)
{
int fd = fds[i].fd;
if(fd == sockfd) //使用accept函数建立连接
{
int connfd = accept(fd, NULL, NULL);
printf("%d is link\n", connfd);
n = connfd - sockfd;
fds[n].fd = connfd;
fds[n].events = POLLIN;
if(m < connfd-2)
{
m = connfd-2;
}
}
else
{
memset(buf, 0, 64);
ret = recv(fd, buf, 64, 0);
if(ret == -1)
{
perror("recv");
return -1;
}
else if(ret == 0)
{
printf("%d is unlink\n", fd);
memset(&fds[i], 0, sizeof(fds[i]));
}
else
{
printf("%d:message:%s\n", fd, buf);
}
}
}
}
}
close(sockfd);
return 0;
}
int tcpser_init(int port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("sockfd");
return -1;
}
printf("sockfd = %d\n", sockfd);
//端口复用函数:解决端口号被系统占用的情况
int on = 1;
int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(k == -1)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in serveraddr,clientaddr;
memset(&serveraddr, 0 ,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址
int len = sizeof(serveraddr);
int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
if(ret == -1)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 100);
if(ret == -1)
{
perror("listen");
return -1;
}
return sockfd;
}
3.poll机制的缺点
poll机制的fds结构体数组有大小限制
三、epoll机制
1.epoll机制的相关函数
(1)epoll使用红黑树来管理所有的文件描述符集合,不受大小限制,更新集合时,不需要重新拷贝整个集合,直接更新红黑树即可(2)利用call back来知道文件描述符是否就绪,只关心已经就绪的文件描述符,不需要遍历所有的文件描述符。
epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait.#include <sys/epoll.h>int epoll_create(int size);功能:创建一个树参数:size:树的节点返回值:成功:文件描述符epfd失败:-1int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);功能:控制epoll参数:epfd: epoll_create的返回值op:EPOLL_CTL_ADD: 添加文件描述符EPOLL_CTL_DEL:删除文件描述符fd: 准备好的文件描述符event:typedef union epoll_data{void *ptr;int fd;uint32_t u32;uint64_t u64;} epoll_data_t;struct epoll_event{uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */};events:EPOLLIN:读事件EPOLLOUT:写事件eg:struct epoll_event ev;ev.events; //事件ev.data.fd; //文件描述符返回值:成功:0失败:-1int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);功能:等待文件描述符就绪参数:epfd: epoll_create的返回值events:struct epoll_event evs[ SIZE ]; //结构体数组maxevents: [ SIZE ]timeout:-1 阻塞,等待文件描述符就绪返回值:成功:准备好的文件描述符失败:-1
2.epoll机制的实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <sys/epoll.h>
int tcpser_init(int port);
int main(int argc, const char *argv[])
{
int sockfd = tcpser_init(11111);
printf("wait a client.............\n");
//epoll机制,实现IO多路复用的并发服务器
int epfd = epoll_create(128);
if(epfd == -1)
{
perror("epoll_create");
return -1;
}
struct epoll_event ev, evs[128];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
char buf[64] = {0};
int m, i, ret;
while(1)
{
m = epoll_wait(epfd, evs, 128, -1);
if(m == -1)
{
perror("epoll_wait");
return -1;
}
for(i=0 ; i<m; i++)
{
if(evs[i].events & EPOLLIN)
{
int fd = evs[i].data.fd;
if(fd == sockfd)
{
//建立连接,用accept函数
int connfd = accept(fd, NULL, NULL);
printf("%d is link\n", connfd);
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
else{
memset(buf, 0 ,sizeof(buf));
ret = recv(fd, buf, sizeof(buf), 0);
if(ret == -1)
{
perror("recv");
return -1;
}
else if(ret == 0)
{
printf("%d is unlink\n", fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
close(fd);
}
else{
printf("%d:message=%s\n", fd, buf);
}
}
}
}
}
close(sockfd);
return 0;
}
int tcpser_init(int port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("sockfd");
return -1;
}
printf("sockfd = %d\n", sockfd);
//端口复用函数:解决端口号被系统占用的情况
int on = 1;
int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(k == -1)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in serveraddr,clientaddr;
memset(&serveraddr, 0 ,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址
int len = sizeof(serveraddr);
int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
if(ret == -1)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 100);
if(ret == -1)
{
perror("listen");
return -1;
}
return sockfd;
}
3.epoll机制的缺点
暂无