订餐网站开发流程,广告设计宣传画册,网站建设的应对措施,贵州省建设厅造价通官方网站TCP/IP通信详解 1 基础1.1 三次握手1.2 四次挥手1.3 Socket实现TCP通信流程 2 Socker基础2.1 Socket地址API2.1.1 字节序2.1.2 主机字节序 网络字节序2.1.3 通用socket地址2.1.4 专用socket地址2.1.5 IP地址转换函数#xff1a; 2.2 socket基础API2.2.1 创建socket… TCP/IP通信详解 1 基础1.1 三次握手1.2 四次挥手1.3 Socket实现TCP通信流程 2 Socker基础2.1 Socket地址API2.1.1 字节序2.1.2 主机字节序 网络字节序2.1.3 通用socket地址2.1.4 专用socket地址2.1.5 IP地址转换函数 2.2 socket基础API2.2.1 创建socketsocket函数2.2.2 socket选项 读取和设定getsocket/setsocket函数2.2.3 命名socketbind函数2.2.4 监听socketlisten函数2.2.5 发起连接connect函数2.2.6 接受连接accept函数2.2.7 读写数据send/recv函数2.2.8 关闭连接close/shutdown函数2.2.9 获取地址信息getsockname/getpeername函数2.2.10 检测带外标记sockmark函数2.2.11 其他 3 TCP/IP通信3.1 IP协议3.1.1 IP头部3.1.2 tcpdump观察IP头部3.1.3 IP路由 3.2 TCP协议3.2.1 TCP头部3.2.2 tcpdump观察tcp头部 3.3 三次握手与四次挥手的socket分析3.3.1 三次握手3.3.2 四次挥手 3.1 服务端3.2 客户端3.3 演示 4 实现并发服务器4.1 多进程解决fork方式4.1.1 服务端代码4.1.2 客户端代码 4.2 基于I/O多路复用:select4.2.1基础4.2.2 select 4.3 异步IO :epoll方式4.3.1 epoll触发方式4.3.2 epoll API4.3.3 epoll工作模式4.3.4 epoll代码 1 基础
三次握手四次挥手
1.1 三次握手 1.2 四次挥手 1.3 Socket实现TCP通信流程
传统的TCP/IP通信过程依赖于socket位于应用层和传输层之间使得应用程序可以进行通信。相当于港口城市的码头使得城市之间可以进行货物流通。服务器和客户端各有不同的通信流程。
Socket编程实现TCP通信的流程如下图 我们的目标就是编程实现这个流程。 2 Socker基础
要想完成网络编程需要了解基础网络编程API
2.1 Socket地址API
socket含义是IP和端口对 ipport它唯一表示了TCP通信的一端。也称为socket地址 要学习socket地址需要先学习主机字节序和网络字节序
2.1.1 字节序
为字节的顺序就是大于一个字节类型的数据在内存中的存放顺序
字节序分为两类大端字节序和小端字节序
Big-Endian是高位字节排放在内存的低地址端低位字节排放在内存的高地址端。Little-Endian就是低位字节排放在内存的低地址端高位字节排放在内存的高地址端。
可以通过代码判断当前主机的字节序类型联合体共用同一内存可以看0x0102如何存储的。
#include stdio.h
#include stdlib.h
int main(void)
{union {short s;char c[sizeof(short)];} un;un.s 0x0102;if (un.c[0] 1 un.c[1] 2){printf(Big-Endian\n);}if (un.c[0] 2 un.c[1] 1){printf(Little-Endian\n);}exit(0);
}2.1.2 主机字节序 网络字节序 现代PC绝大多数都是小端的字节序因此小端字节序称为主机字节序 大端也称为网络字节序。 多台电脑进行通信时如果双方是不同的字节序会造成通信解析错误。解决办法发送端总是采用大端字节序接收端根据自身的字节序决定是否对接收的数据进行转换小端转换大端不转
为了进行转换 bsd socket提供了转换的函数 有下面四个
htons 把unsigned short类型从主机序转换到网络序htonl 把unsigned long类型从主机序转换到网络序ntohs 把unsigned short类型从网络序转换到主机序ntohl 把unsigned long类型从网络序转换到主机序
长整型通常转换IP短整型转换端口
2.1.3 通用socket地址
表示socket地址的是结构体socketaddr
#include bits/socket.h
struct socketaddr
{sa_family_t sa_family;char sa_data[14];
}sa_family是地址族变量通常地址族和协议族对应 sa_data存放socket地址值不同的协议对应的地址值具有不同的含义和地址
2.1.4 专用socket地址
上述的通用地址不好用故针对UNIX、IPv4、IPv6分别定义了不同的专用socket地址结构体此时列出IPv4的结构体socketaddr_in
struct socketaddr_in
{sa_family_t sin_family;u_int16_t sin_port;struct in_addr sin_addr;
}
struct in_addr
{u_int32_t s_addr;
}所有专用的socket地址类型的变量在实际使用时都要转化为通用socket地址类型socketaddr强制类型转换 2.1.5 IP地址转换函数
人们习惯字符串表示IP地址
点分十进制表示IPv4点分十六进制表示IPv6
但是编程中我们需要字符串转为整数二进制。日志中则需要把整数IP转为字符串。 因此三个函数可以将点分十进制字符串表示的IPv4地址和用网络字节序整数表示的地址进行转换 如inet_addr输入点分十进制字符串返回网络字符串整数。失败则返回INADDR_NONE
2.2 socket基础API
包括
创建socketsocket函数socket选项 getsocket/setsocket函数命名socketbind函数监听socketlisten函数接受连接accept函数发起连接connect函数读写数据send、recv函数关闭连接close/shutdown函数获取地址信息getsockname/getpeername函数检测带外标记sockmark函数
返回值若为errnoLinux提供的表示各种错误
2.2.1 创建socketsocket函数
Linux哲学所有东西都是文件 socket是可读可写可控制可关闭的文件描述符使用socket系统调用创建一个socket
domain参数是用哪个底层协议族type是指定服务类型如SOCK-STREAM(流服务TCP) / SOCK_UGRAM(数据报UDP)新内核中可以与 SOCK_NONBLOCK和SOCK_CLOEXEC标志与protocol是在前两个协议下再选择一个具体协议默认协议为0系统调用成功则返回一个socket文件描述符失败则返回-1并设置errno
如图为系统调用TCP协议下IPv4的socket 2.2.2 socket选项 读取和设定getsocket/setsocket函数
专门读取和设定socket文件描述符属性
sockfd指定被操作的目标socketlevel指定操作那个协议的选项比如IPv4/IPv6/TCP等option_name指定选项的名字option_value和option_len分别是被操作选项的值和长度。成功时都是返回0否则返回-1且设置errno
重要的几个选项
SO_REUSEADDR端口处于WAIT_TIME仍然可以启动SO_RCVBUFTCP接收缓冲区大小SO_SNDBUF发送缓冲区大小
2.2.3 命名socketbind函数
创建socket指定了地址族但没有指定具体socket地址。将socket指定具体的socket地址称为命名绑定。只有服务器端命名后客户端才知道如何连接它。客户端一般不绑定而是匿名方式自动分配socket地址 bind将my_addr所指向的socket地址分配给未命名的sockfd文件描述符addrlen指出该socket地址长度。
bind成功返回0失败返回-1并设置errno
EACCES 被绑定的socket地址是受保护的地址仅超级用户可以访问如绑定知名服务端口0~1023EADDRINUSE 被绑定的地址正在使用中如将socket绑定到一个处于time_wait状态的socket地址
2.2.4 监听socketlisten函数
被命名后还不能马上连接客户端还需要系统调用创建监听队列存放待处理的客户连接
sockfd指定被监听的socketbacklog提示内核监听队列的最大长度典型值为5超过长度则不接受新的客户连接客户端收到ECONNREFUSED错误成功则返回0失败返回-1且设置errno
eg10个客户端连接服务器最大值设为5则6个(51)处于完全连接ESTABLISHED5个处于半连接状态SYN_RCVD
2.2.5 发起连接connect函数
服务器通过listen被动接受连接客户端用connect主动与服务端建立连接
sockfd是2.2.1socket函数系统调用后返回的socketserv_addr服务器监听的socket地址addrlen指定了地址长度成功则返回0否则返回-1且设置errno常见的 1.ECONNREFUSED 目标端口不存在 2.ETIMEDOUT 链接超时一旦成功建立sockfd唯一标识了这个连接客户端通过读写sockfd与服务区通信
2.2.6 接受连接accept函数
函数从监听队列中接收一个进行连接
sockfd是listen中的第一个监听参数socketaddr获取被连接的远端socket地址长度是addrlen如果是专用socket结构体需要强制类型转换accept成功则返回新的socket失败则返回-1且设置errno
2.2.7 读写数据send/recv函数
文件操作read和write也可以使用于socket但是他也有专用的API其中用于TCP流数据读写的系统调用是
recv读取sockfd上数据buf和len分别制定缓冲区的位置和大小recv成功则返回读取的长度它可能小于我们期望的长度故需要多次调用recv才能完整读取。返回0这说明通信对方关闭了。返回-1则出错了且设置errnosend往sockfd写数据buf和len指定写数据的缓冲区位置和大小。send成功返回时及写入的数据长度失败则返回-1且设置errno。flags是为数据收发提供了额外的控制
2.2.8 关闭连接close/shutdown函数
关闭这个连接实际就是关闭连接对应的socket可以使用普通文件描述符
fd是待关闭的socket不是立即关闭该链接而是fd的引用计数减1. 只有当fd引用计数为0真正关闭。多进程程序中fork调用会使父进程中计数1只有父子进程均close关闭socket才算退出。如果想立即退出则使用shutdown系统调用相对于close它是专门的网络编程设计 sockfd是待关闭的sockethowto则决定了shutdown的行为shutdown成功返回0失败返回-1且设置errno
2.2.9 获取地址信息getsockname/getpeername函数 2.2.10 检测带外标记sockmark函数 2.2.11 其他
通用数据读写函数 UDP读写 网络信息API:主机名访问机器服务名称访问端口号 等等。
3 TCP/IP通信
3.1 IP协议
IP协议是TCP/IP协议族的核心协议也是socket基础。
IP头部信息指定源端、目的端IP地址IP切片等IP数据报路由和转发决定数据报是否应该转发和如何转发。
IP协议为上层协议提供无状态、无连接、不可靠的服务。
3.1.1 IP头部
主要是IPv4
长度一般20字节除非有可变长的选项4位版本号指定IP协议的版本IPv4为44位头部长度该IP头部有多少个4字节32位4位最大15故最大15*460字节8位服务类型如最小延时ssh、telnet、最大吞吐量ftp16位总长度是IP数据报的长度受MTU限制长度超过MTU则分片传输实际传输的IP数据报长度都没有超过最大值接下三个字段表示分片16位标识唯一标识主机发送的每一个数据报系统随机生成每发送一个数据报值加1。如果分片则每个数据报标识相同。3位标识第一字段保留、第二字段进制分片第三标识更多分片13位分片偏移分片相对于原始数据报的偏移。8位生存时间数据报到达目的地之前允许的最大路由器跳数。TTL减为0发送ICMP差错报文。8位协议区分上层协议ICMP1,TCP6,UDP17等。16位头部校验和检验IP数据报头部在传输过程是否损坏。32位的源端IP和目的端IP表示数据包的发送端和接收端整个传输保持不变。最后是选项字段最多包含40字节如记录路由、时间戳、松散路由源选择等
3.1.2 tcpdump观察IP头部
执行telnet登陆本机抓取客户机与服务器之间交换的数据包 源端IP和目的端IP都是127.0.0.1服务器端口23客户端使用临时端口58422与服务器进行通信抓包开启了x选型告诉tcpdump命令需要把协议头和包内容都原原本本的显示出来tcpdump会以16进制和ASCII的形式显示这在进行协议分析时是绝对的利器。此数据包60字节。前20字节是IP头部后40字节是TCP头部
可参考如下分析 3.1.3 IP路由
决定数据报是否应该转发和如何转发。 路由工作流程、路由机制、IP转发、ICMP重定向等 路由相关命令route、netstat等
3.2 TCP协议
和IP协议相比更靠近应用层。
TCP头部指定源端、目的端端口号管理TCP控制数据流等TCP状态转移连接的任意一段都是状态机。连接到断开经历不同的状态变迁。TCP数据流交互数据流和成块数据流、紧急数据流数据流的控制超时重传和拥塞控制
3.2.1 TCP头部 16位端口号:告知该报文来自哪个源端口传给哪个上层协议或目的端口。通信时服务端一般采用知名的服务端口/etc/services客户端使用系统自动选择的临时端口号。32位序号一次TCP通信连接到断开过程中某个传输方向上的字节流的字节编号。A发给B的报文段中序号值将被系统设置ISN加上该报文段的偏移如传的1025~2048则传输ISN1025。32位确认号用作对另一方发来报文段的响应。A发送的不仅携带自己的序号也携带对B发送的数据的确认号。返回值发送14位头部长度四位最大60头部最长60字节6位标志位 1URG 紧急指针是否有效 2ACK表示确认号是否有效 3PSH表示李继聪TCP接收缓冲区读走数据 4RST要求对方重新建立连接 5SYN请求建立一个连接携带SYN标志的TCP报文段为同步报文。 6FIN结束连接携带FIN为结束报文段。16位窗口大小TCP流量控制的手段16位校验和检验TCP报文段是否损坏可靠传输的保证16位紧急指针正的偏移量它和序号字段的和表示最后一个紧急数据的下一字节序号TCP头部选项最多包含40字节总共60字节前面固定长度20字节1字节8位
3.2.2 tcpdump观察tcp头部
执行telnet登陆本机抓取客户机与服务器之间交换的数据包 -输出Flags[S]表示含SYN标志它是同步报文段。如果也含其他标志则会显示其他标志的首字母
seq是序号值因为这是第一个报文所以它是ISN随机值也没有确认值win是接收通告窗口大小options是TCP选项
参考分析如下
3.3 三次握手与四次挥手的socket分析
3.3.1 三次握手 通过tcpdump抓取通信过程
3.3.2 四次挥手 通过tcpdump抓取通信过程
3.1 服务端
#include iostream
#include sys/socket.h
#include netinet/in.h//struct
#include unistd.h//close head
#include string.h//bzero
#include stdlib.h
#define PORT 8111
#define MESSAGE_SIZE 1024
int main(int argc, char* argv[])
{int socket_fd;int accept_fd;int backlog 10;int ret -1;int flag 1;struct sockaddr_in local_addr, remote_addr;char in_buf[MESSAGE_SIZE] {0,};//create socketsocket_fd socket(AF_INET, SOCK_STREAM, 0);if (socket_fd -1){std::cout file to create socket! std::endl;exit(-1);}//set socket optionsret setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, flag, sizeof(flag));if (ret -1){std::cout failed to set socket options! std::endl;}//set localaddrlocal_addr.sin_family AF_INET;local_addr.sin_port PORT;local_addr.sin_addr.s_addr INADDR_ANY;//0 RENHE IP DOU LISTENbzero((local_addr.sin_zero),8);//bind socketret bind(socket_fd, (struct sockaddr *)local_addr, sizeof(struct sockaddr));if (ret -1){std::cout failed to bind addr! std::endl;exit(-1);}//listenret listen(socket_fd, backlog);if (ret -1){std::cout failed to listen socket! std::endl;exit(-1);}for(;;){socklen_t addr_len sizeof(struct sockaddr);accept_fd accept(socket_fd, (struct sockaddr *) remote_addr, addr_len);for (;;){ret recv(accept_fd, (void *)in_buf, MESSAGE_SIZE, 0);if (ret 0)break;std::cout receive: in_buf std::endl;send(accept_fd, (void*)in_buf, MESSAGE_SIZE,0);}close(accept_fd);}close(socket_fd);return 0;
}
3.2 客户端
#include iostream
#include sys/socket.h//1connect
#include sys/types.h//2connect
#include netinet/in.h //struct
#include string.h//memset
#include stdio.h//gets
#include unistd.h//close
#include arpa/inet.h//inet
#include stdlib.h
#define PORT 8111
#define MESSAGE_LEN 1024
using namespace std;
int main(int argc, char* argv[])
{int socket_fd;int ret -1;char sendbuf[MESSAGE_LEN] {0,};char recvbuf[MESSAGE_LEN] {0,};struct sockaddr_in serverAddr;socket_fd socket(AF_INET, SOCK_STREAM, 0);if (socket_fd 0){cout failed to create socket endl;exit(-1);}serverAddr.sin_family AF_INET;serverAddr.sin_port PORT;serverAddr.sin_addr.s_addr inet_addr(127.0.0.1);ret connect(socket_fd, (struct sockaddr *)serverAddr, sizeof(struct sockaddr));if (ret 0){cout failed to connect! endl;exit(-1);}while(1){memset(sendbuf, 0, MESSAGE_LEN);//gets(sendbuf); scanf(%s, sendbuf[0]);ret send(socket_fd, sendbuf, strlen(sendbuf), 0);if (ret 0){cout failed to send data! endl;break; }//guanbi 1 kill -9 1111 2 duibiif (strcmp(sendbuf, quit) 0){break;}ret recv(socket_fd, recvbuf, MESSAGE_LEN, 0);recvbuf[ret] \0;cout recv: recvbuf endl;}close(socket_fd);return 0;
}3.3 演示 4 实现并发服务器
在3中所演示的C/S通信架构只有一对server和client时适用当多个client发出连接请求则会失败只有client结束其余client才能抢占资源。
4.1 多进程解决fork方式
解决方案
每收到一个连接就创建子进程父进程负责接受连接通过fork创建子进程
存在问题
资源被长期占用只要长连接没有断开则子进程一直被占用。上万个被占用怎么办创建子进程花费时间长大量连接父进程必须关闭已连接描述符以防止内存泄漏知道父子所有进程连接描述符关闭连接才会终止。
4.1.1 服务端代码
#include iostream
#include sys/socket.h
#include netinet/in.h//struct
#include unistd.h//close head
#include string.h//bzero
#define PORT 8111
#define MESSAGE_SIZE 1024
int main(int argc, char* argv[])
{int socket_fd;int accept_fd;int backlog 10;int ret -1;int flag 1;pid_t pid;struct sockaddr_in local_addr, remote_addr;char in_buf[MESSAGE_SIZE] {0,};//create socketsocket_fd socket(AF_INET, SOCK_STREAM, 0);if (socket_fd -1){std::cout file to create socket! std::endl;exit(-1);}//set socket optionsret setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, flag, sizeof(flag));if (ret -1){std::cout failed to set socket options! std::endl;}//set localaddrlocal_addr.sin_family AF_INET;local_addr.sin_port PORT;local_addr.sin_addr.s_addr INADDR_ANY;//0 RENHE IP DOU LISTENbzero((local_addr.sin_zero),8);//bind socketret bind(socket_fd, (struct sockaddr *)local_addr, sizeof(struct sockaddr));if (ret -1){std::cout failed to bind addr! std::endl;exit(-1);}//listenret listen(socket_fd, backlog);if (ret -1){std::cout failed to listen socket! std::endl;exit(-1);}for(;;){socklen_t addr_len sizeof(struct sockaddr);accept_fd accept(socket_fd, (struct sockaddr *) remote_addr, addr_len); pid fork();if (pid 0){for (;;)//loop daozhi zhiyou yige client{ret recv(accept_fd, (void *)in_buf, MESSAGE_SIZE, 0);if (ret 0)break;std::cout receive: in_buf std::endl;send(accept_fd, (void*)in_buf, MESSAGE_SIZE,0);}close(accept_fd);} }if (pid!0)//fu jincheng guanbi{close(socket_fd);}return 0;
}4.1.2 客户端代码
不变
4.2 基于I/O多路复用:select
异步IO指以事件触发的方式对IO操作进行处理 如同时响应两个事件
网络客户端发起连接请求用户在键盘输入命令行
与多进程/多线程相比呢异步IO技术
系统开销小不必创建进程/线程不必维护他们
select方式
遍历文件描述符集所有描述符找出有变化的描述符对于侦听的socket和数据处理的socket区别对待socket设置为非阻塞。
在多进程多线程方式中接受连接发起连接发送接收数据可能是阻塞的因此后面对accept的socket有进行非阻塞式设置
4.2.1基础
fcntl函数 功能描述根据文件描述词来操作文件的特性。
fcntl函数有5种功能
复制一个现有的描述符cmdF_DUPFD.获得设置文件描述符标记(cmdF_GETFD或F_SETFD).获得设置文件状态标记(cmdF_GETFL或F_SETFL).获得设置异步I/O所有权(cmdF_GETOWN或F_SETOWN).获得设置记录锁(cmdF_GETLK,F_SETLK或F_SETLKW).
flags fcntl(socket_fd, F_GETFL, 0);
fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK);//FEI ZU SE FANGSHI此处将socket_fd设置为非阻塞式socket
select函数
该函数准许进程指示内核等待多个事件中的任何一个发送并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下
#include sys/select.h
#include sys/time.h
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值就绪描述符的数目超时返回0出错返回-1第一个参数待测试的最大描述字加1fd_set是理解为一个集合这个集合中存放的是文件描述符可通过以下四个宏进行设置
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写 timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。1永远等待下去2等待一段固定时间3根本不等待
原理 4.2.2 select
第一步
定义accept_fd[FD_SIZE] {-1}FD_SIZE默认定义1024最多1024个连接弊端
第二步
iocntl设定为非阻塞式
第四步侦听之后
首先FD_ZERO清空存放文件描述符的集合然后通过FD_SET将socket_fd加入集合for循环1024.如果每个不等于-1是一个有效的socket则将其加入FD_SET如果大于max_fd替换 判断select返回值是否0判断socket_fd是否可读写可以的话找空槽-1的并记录将接受连接返回的新socket设置为非阻塞 如果当前socket有效且可读写则进接收发送数据
#include iostream
#include sys/socket.h
#include netinet/in.h//struct
#include unistd.h//close head
#include string.h//bzero
#include fcntl.h//select
#include stdlib.h
#define PORT 8111
#define MESSAGE_SIZE 1024
#define FD_SIZE 1024
int main(int argc, char* argv[])
{int socket_fd -1;int accept_fd -1;int backlog 10;int ret -1;int flag 1;int maxpos 0;int events 0;int max_fd -1;int curpos -1;int flags;fd_set fd_sets;int accept_fds[FD_SIZE] {-1,};struct sockaddr_in local_addr, remote_addr;char in_buf[MESSAGE_SIZE] {0,};//create socketsocket_fd socket(AF_INET, SOCK_STREAM, 0);if (socket_fd -1){std::cout file to create socket! std::endl;exit(-1);}//set socket optionsret setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, flag, sizeof(flag));if (ret -1){std::cout failed to set socket options! std::endl;}//flags fcntl(socket_fd, F_GETFL, 0);fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK);//FEI ZU SE FANGSHI////set localaddrlocal_addr.sin_family AF_INET;local_addr.sin_port PORT;local_addr.sin_addr.s_addr INADDR_ANY;//0 RENHE IP DOU LISTENbzero((local_addr.sin_zero),8);//bind socketret bind(socket_fd, (struct sockaddr *)local_addr, sizeof(struct sockaddr));if (ret -1){std::cout failed to bind addr! std::endl;exit(-1);}//listenret listen(socket_fd, backlog);if (ret -1){std::cout failed to listen socket! std::endl;exit(-1);}max_fd socket_fd;for(int i0; i FD_SIZE; i){accept_fds[i] -1; } for(;;){FD_ZERO(fd_sets);FD_SET(socket_fd, fd_sets);for (int i 0; i maxpos; i){if (accept_fds[i] ! -1){if (accept_fds[i] max_fd){max_fd accept_fds[i];}FD_SET(accept_fds[i], fd_sets);}}events select(max_fd1, fd_sets, NULL, NULL, NULL);if (events 0){std::cout failed to use select std::endl;break;}else if (events 0){std::cout timeout... std::endl;continue;}else if (events){if (FD_ISSET(socket_fd, fd_sets)){for (int i 0; i FD_SIZE; i){if (accept_fds[i] -1){curpos i;break;}}socklen_t addr_len sizeof(struct sockaddr);accept_fd accept(socket_fd, (struct sockaddr *) remote_addr, addr_len);flags fcntl(accept_fd, F_GETFL, 0);fcntl(accept_fd, F_SETFL, flags | O_NONBLOCK);//FEI ZU SE FANGSHIaccept_fds[curpos] accept_fd;if(curpos1 maxpos){maxpos curpos 1; }if(accept_fd max_fd){max_fd accept_fd; }}for (int i 0; i FD_SIZE; i){if (accept_fds[i] ! -1 FD_ISSET(accept_fds[i], fd_sets)){memset(in_buf, 0, MESSAGE_SIZE);ret recv(accept_fds[i], (void *)in_buf, MESSAGE_SIZE, 0);if (ret 0){close(accept_fds[i]);accept_fds[i] -1;break;}std::cout receive: in_buf std::endl; send(accept_fds[i], (void*)in_buf, MESSAGE_SIZE,0);}}} }close(socket_fd);return 0;
}基于上面的讨论可以轻松得出select模型的特点
1)可监控的文件描述符个数取决与sizeof(fd_set)的值。但调整上限受于编译内核时的变量值。
2将fd加入select监控集的同时还要再使用一个数据结构array保存放到select监控集中的fd
一是用于再select 返回后array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空则每次开始 select前都要重新从array取得fd逐一加入FD_ZERO最先扫描array的同时取得fd最大值maxfd用于select的第一个 参数。
可见select模型必须在select前循环array加fd取maxfdselect返回后循环arrayFD_ISSET判断是否有事件发生。
缺点
文件符默认1024数量太少半自动方式找到真正触发的符但仍然比fork高效
4.3 异步IO :epoll方式
epoll是在2.6内核中提出的是之前的select和poll的增强版本。相对于select和poll来说epoll更加灵活没有描述符限制。epoll使用一个文件描述符管理多个描述符将用户关系的文件描述符的事件存放到内核的一个事件表中这样在用户空间和内核空间的copy只需一次。
没有文件描述符限制工作效率不会随着文件描述符增加而下降epoll经过内核级的系统优化更搞笑
4.3.1 epoll触发方式
Level Trigger 没有处理反复发送 水平触发开发难度小Edge Trigger 只发送一次 边缘触发开发难度大
4.3.2 epoll API
#include sys/epoll.h
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);epoll详解
epoll_ctl:第二个参数 EPOLL_CTL_ADD注册新的fd到epfd中 EPOLL_CTL_MOD修改已经注册的fd的监听事件 EPOLL_CTL_DEL从epfd中删除一个fd 第三个参数epoll事件
4.3.3 epoll工作模式
epoll对文件描述符的操作有两种模式LTlevel trigger和ETedge trigger。LT模式是默认模式LT模式与ET模式的区别如下
LT模式当epoll_wait检测到描述符事件发生并将此事件通知应用程序应用程序可以不立即处理该事件。下次调用epoll_wait时会再次响应应用程序并通知此事件。ET模式当epoll_wait检测到描述符事件发生并将此事件通知应用程序应用程序必须立即处理该事件。如果不处理下次调用epoll_wait时不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数因此效率要比LT模式高。epoll工作在ET模式的时候必须使用非阻塞套接口以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
4.3.4 epoll代码
Linux高性能服务器