1 基础
1.1 三次握手
1.2 四次挥手
1.3 Socket实现TCP通信流程
传统的TCP/IP通信过程依赖于socket,位于应用层和传输层之间,使得应用程序可以进行通信。相当于港口城市的码头,使得城市之间可以进行货物流通。服务器和客户端各有不同的通信流程。
Socket编程实现TCP通信的流程如下图:
我们的目标就是编程实现这个流程。
2 Socker基础
要想完成网络编程,需要了解基础:网络编程API
2.1 Socket地址API
socket含义是:IP和端口对 (ip,port),它唯一表示了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
包括
- 创建socket:socket函数
- socket选项 :getsocket/setsocket函数
- 命名socket:bind函数
- 监听socket:listen函数
- 接受连接:accept函数
- 发起连接:connect函数
- 读写数据:send、recv函数
- 关闭连接:close/shutdown函数
- 获取地址信息:getsockname/getpeername函数
- 检测带外标记:sockmark函数
返回值若为errno(Linux提供的),表示各种错误
2.2.1 创建socket:socket函数
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指定被操作的目标socket
- level指定操作那个协议的选项,比如IPv4/IPv6/TCP等
- option_name指定选项的名字
- option_value和option_len分别是被操作选项的值和长度。
- 成功时都是返回0,否则返回-1且设置errno
重要的几个选项:
- SO_REUSEADDR:端口处于WAIT_TIME仍然可以启动
- SO_RCVBUF:TCP接收缓冲区大小
- SO_SNDBUF:发送缓冲区大小
2.2.3 命名socket:bind函数
创建socket指定了地址族,但没有指定具体socket地址。将socket指定具体的socket地址称为命名(绑定)。只有服务器端命名后,客户端才知道如何连接它。客户端一般不绑定,而是匿名方式自动分配socket地址
bind将my_addr所指向的socket地址分配给未命名的sockfd文件描述符,addrlen指出该socket地址长度。
- bind成功返回0,失败返回-1并设置errno
- EACCES 被绑定的socket地址是受保护的地址,仅超级用户可以访问,如绑定知名服务端口0~1023
- EADDRINUSE 被绑定的地址正在使用中,如将socket绑定到一个处于time_wait状态的socket地址
2.2.4 监听socket:listen函数
被命名后,还不能马上连接客户端,还需要系统调用创建监听队列存放待处理的客户连接
- sockfd指定被监听的socket
- backlog提示内核监听队列的最大长度,典型值为5(超过长度则不接受新的客户连接,客户端收到ECONNREFUSED错误)
- 成功则返回0,失败返回-1且设置errno
eg:10个客户端连接服务器,最大值设为5,则6个(5+1)处于完全连接(ESTABLISHED),5个处于半连接状态(SYN_RCVD)
2.2.5 发起连接:connect函数
服务器通过listen被动接受连接,客户端用connect主动与服务端建立连接:
- sockfd是2.2.1socket函数系统调用后返回的socket
- serv_addr服务器监听的socket地址,addrlen指定了地址长度
- 成功则返回0,否则返回-1且设置errno,常见的
1.ECONNREFUSED 目标端口不存在
2.ETIMEDOUT 链接超时 - 一旦成功建立,sockfd唯一标识了这个连接,客户端通过读写sockfd与服务区通信
2.2.6 接受连接:accept函数
函数从监听队列中接收一个进行连接:
- sockfd是listen中的第一个监听参数socket
- addr获取被连接的远端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则出错了,且设置errno
- send往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是待关闭的socket,howto则决定了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为4)
- 4位头部长度:该IP头部有多少个4字节(32位)(4位最大15,故最大15*4=60字节)
- 8位服务类型:如最小延时(ssh、telnet)、最大吞吐量(ftp)
- 16位总长度是IP数据报的长度,受MTU限制,长度超过MTU则分片传输(实际传输的IP数据报长度都没有超过最大值)接下三个字段表示分片
- 16位标识:唯一标识主机发送的每一个数据报,系统随机生成;每发送一个数据报,值加1。如果分片,则每个数据报标识相同。
- 3位标识,第一字段保留、第二字段进制分片,第三标识更多分片
- 13位分片偏移分片相对于原始数据报的偏移。
- 8位生存时间:数据报到达目的地之前允许的最大路由器跳数。TTL减为0,发送ICMP差错报文。
- 8位协议区分上层协议,ICMP=1,TCP=6,UDP=17等。
- 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,则传输ISN+1025。
- 32位确认号:用作对另一方发来报文段的响应。A发送的不仅携带自己的序号,也携带对B发送的数据的确认号。返回值=发送+1
- 4位头部长度:四位最大60,头部最长60字节
- 6位标志位
1)URG :紧急指针是否有效
2)ACK:表示确认号是否有效
3)PSH:表示李继聪TCP接收缓冲区读走数据
4)RST:要求对方重新建立连接
5)SYN:请求建立一个连接,携带SYN标志的TCP报文段为同步报文。
6)FIN:结束连接,携带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 socket
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1)
{
std::cout << "file to create socket!" << std::endl;
exit(-1);
}
//set socket options
ret = setsockopt(socket_fd,
SOL_SOCKET,
SO_REUSEADDR,
&flag,
sizeof(flag));
if (ret == -1)
{
std::cout << "failed to set socket options!" << std::endl;
}
//set localaddr
local_addr.sin_family = AF_INET;
local_addr.sin_port = PORT;
local_addr.sin_addr.s_addr = INADDR_ANY;//0 RENHE IP DOU LISTEN
bzero(&(local_addr.sin_zero),8);
//bind socket
ret = bind(socket_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
if (ret == -1)
{
std::cout << "failed to bind addr!" << std::endl;
exit(-1);
}
//listen
ret = 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 duibi
if (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 socket
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1)
{
std::cout << "file to create socket!" << std::endl;
exit(-1);
}
//set socket options
ret = setsockopt(socket_fd,
SOL_SOCKET,
SO_REUSEADDR,
&flag,
sizeof(flag));
if (ret == -1)
{
std::cout << "failed to set socket options!" << std::endl;
}
//set localaddr
local_addr.sin_family = AF_INET;
local_addr.sin_port = PORT;
local_addr.sin_addr.s_addr = INADDR_ANY;//0 RENHE IP DOU LISTEN
bzero(&(local_addr.sin_zero),8);
//bind socket
ret = bind(socket_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
if (ret == -1)
{
std::cout << "failed to bind addr!" << std::endl;
exit(-1);
}
//listen
ret = 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种功能:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_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
- 第一个参数:待测试的最大描述字加1
- fd_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 socket
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1)
{
std::cout << "file to create socket!" << std::endl;
exit(-1);
}
//set socket options
ret = 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 localaddr
local_addr.sin_family = AF_INET;
local_addr.sin_port = PORT;
local_addr.sin_addr.s_addr = INADDR_ANY;//0 RENHE IP DOU LISTEN
bzero(&(local_addr.sin_zero),8);
//bind socket
ret = bind(socket_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
if (ret == -1)
{
std::cout << "failed to bind addr!" << std::endl;
exit(-1);
}
//listen
ret = listen(socket_fd, backlog);
if (ret == -1)
{
std::cout << "failed to listen socket!" << std::endl;
exit(-1);
}
max_fd = socket_fd;
for(int i=0; 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_fd+1, &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 FANGSHI
accept_fds[curpos] = accept_fd;
if(curpos+1 > 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,取maxfd),select返回后循环array(FD_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_ctl:第二个参数:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数:epoll事件:
4.3.3 epoll工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。