计算机网络
1. TCP/IP协议
TCP总结
首先IP层是负责IP到IP的数据包传输的,只能区分两个不同的主机,如果想要两个主机中的进程间传输数据,还需要对进程进行区分,这就需要三元组(IP+协议+端口)来实现网络间进程的唯一识别,而协议+端口就是通过传输层来实现的,TCP可以收包后确认、丢包后重发、由于带宽和不同机器处理能力不同,TCP还要控制流量,另外,TCP接收到应用层传过来的字节流后分割成数据包,然后传输的是数据包,IP层传输是无法保证数据包传输的顺序以及可能会导致数据包重复,这些都需要TCP进行控制,保证能够将字节流还原。
TCP有六个标志位:URG, ACK, PSH, RST, SYN, FIN
URG: 表示紧急指针域有效,保证TCP连接不被中断,督促中间层尽快处理这些数据
ACK: 确认位
PSH: 表示接收端收到该数据包后,不在缓冲区中等待,直接传给应用层处理
RST: 连接复位请求,复位异常连接,拒绝错误、非法的数据包
SYN: 用来建立连接的同步序号
FIN: 表示发送端已经达到发送数据的末尾,没有数据可传了,可以断开连接了
TCP握手状态图
上图中虚线表示服务端的状态转移路线,实线表示客户端的状态转移曲线,介绍下几个状态的含义:
CLOSED:服务端和客户端的初始状态。
LISTEN:服务端进入监听状态监听连接请求。
SYN_RCVD:服务端在接收到客户端的SYN连接请求后发送SYN+ACK给客户端,然后进入该状态,在接收到客户端的ACK报文之后进入ESTABLISHED状态。
SYN_SENT:客户端在发送出连接请求SYN后进入该状态,当接收到服务端的SYN+ACK后,返回ACK确认报文,然后进入ESTABLISHED状态。
ESTABLISHED:连接已建立。
FIN_WAIT_1:客户端主动发出关闭连接的请求FIN后进入该状态,当收到服务端的ACK确认报文后,客户端进入TIME_WAIT_2状态。注意如果这一阶段先收到了服务端发来的FIN,那么会先进入CLOSING状态,等接受到服务端的ACK后才能进入TIME_WAIT状态,如果同时接收到了ACK+FIN,那么跳过TIME_WAIT_2直接进入TIME_WAIT状态。
FIN_WAIT_2:客户端在收到服务端发送的FIN报文后,返回ACK确认报文,然后从该状态进入到TIME_WAIT状态。
TIME_WAIT:当客户端接收到了服务端发来的FIN报文,同时发送了ACK报文之后,客户端进入了TIME_WAIT状态,等待2MSL就可以进入CLOSED状态了。一般1MSL是1~2分钟,之所以需要等待一段时间才能进入CLOSED状态是因为客户端发送出ACK报文之后,可能由于网络不稳定等原因导致服务端没接受到,这时服务端会重新发送FIN报文,TIME_WAIT状态就是用来重发可能丢失的ACK报文的。
CLOSING:这种情况比较少见,按说从TIME_WAIT_1状态应该先接受到ACK报文,然后进入TIME_WAIT_2,等对方数据全部发完然后发个FIN过来,但是现在却先接受到了FIN,这种情况是由于双方同时主动关闭连接,所以都会发一个FIN给对方,这时返回一个ACK后进入CLOSING状态,表示双方都在关闭连接,当接收到对方的ACK后进入TIME_WAIT状态。
CLOSE_WAIT:服务端在接收到客户端的关闭连接请求FIN后,首先返回ACK确认,然后进入CLOST_WAIT状态,等服务端数据全部发送完可以中断连接的时候,会发送一个FIN给客户端,这时服务端从该状态进入LAST_ACK状态。
LAST_ACK:服务端在发送了FIN报文后会进入这一状态,等待客户端确认,接收到确认ACK之后进入CLOSED状态。
详解TCP三次握手:
A向B发起建立连接请求,首先要将标志位SYN置为1,然后跟一个seq序号,如果当前的seq=100,发送的TCP数据包大小是100字节,那么下次的TCP序号会变为seq=200,mss段表示A接收TCP数据包的最大字节数限制。win表示A的滑动窗口大小,滑动窗口用来加速数据传输,比如A发送了seq=100的数据包,理应在接受到B返回的seq=101的确认信号后再发送下一个数据包,但是有了滑动窗口,只要在B的滑动窗口中新包的seq与窗口中seq最小的旧包seq之差不超过滑动窗口的大小,就可以继续发送。并且接收端B也可以不用对每个数据包都返回ACK确认,只需要接收多个包,然后对最后一个包进行确认。
第二次握手完成了半双工连接,第三次握手后才完成了全双工连接。
详解四次握手:
重点讲一下FIN标志位和RST标志位的区别,FIN标志位在缓冲区是按照顺序发送的,也就是缓冲区中前面的数据全部发送完毕后才发送FIN,而发送RST标志位会直接丢弃前面的所有数据。
RST重置连接
FIN是正常关闭连接请求,需要四次握手。而RST是异常关闭连接请求,首先发送端在发出RST包的时候会丢弃缓冲区中的包,然后接收端在接收到RST包后也不需要返回ACK进行确认。TCP处理程序会在自己认为异常的时刻发送RST包,比如:
1. 端口未打开:A向B的某个端口发起连接请求,但是B中压根没有监听这个端口,这时B的TCP处理程序会给A发送一个RST包;
2. 连接超时:当客户端接收数据超时后,会向服务端发送一个RST重置连接;
3. 半连接打开:A和B正在进行数据传输,然后网断了,这时A由于某些原因重启连接,等网通了之后,B又开始发送数据,A这时无法识别这是哪个连接了,会发送给B一个RST包,B收到后断开重连。
TCP协议漏洞
1. 半连接
未连接队列
三次握手的过程中,第一次握手客户端给服务端发起连接请求,服务端会返回一个SYN+ACK,然后等待客户端的ACK确认,这时通过两次握手已经建立了半双工连接,这个状态叫做半连接。服务端会维护一个未连接队列,队列里面的条目表示的是当前服务端等待客户端的确认,当服务端收到了ACK确认后,该条目会在未连接队列中删除,然后服务器进入ESTABLISHED状态。
backlog表示未连接队列的最大容纳数目。
SYN-ACK重传次数
如果服务端没收到客户端的确认消息,那么服务端会进行首次重传,过一段时间如果还没收到消息,那么服务端会进行二次重传,不过每次等待重传时间不一定相同,如果重传次数达到最大重传次数,那么服务端会将这个连接信息从未连接队列中删除。
半连接存活时间
指的是半连接队列中的条目的最长存活时间,也就是该条目重传等待时间总和。半连接存活时间又称为Timeout时间。
基于半连接的SYN攻击
就是仿造发送端向目的端发送SYN连接请求,然后服务端会返回SYN+ACK,但是由于发送端是仿造的,所以不会回复ACK确认,这样未连接队列中的连接条目会不断增加,导致正常的SYN请求无法接收,并且服务端运行缓慢,可能会导致瘫痪。
2. RST复位攻击
原理很简单,就是在客户端A和服务端B进行通信的时候,伪装成A给B发个RST复位标志,迫使B断开连接,A和B的连接是建立在源端口+源IP+目的端口+目的IP上的,服务端的IP和端口好确定,源IP也好确定,但是这个源端口不好确定,然后还得保证伪造的RST包必须在服务端B的滑动窗口之内,因为滑动窗口之外的包是会被丢弃的。
解决TCP漏洞的方法
1. 设置防火墙网关过滤
网关既可以在防火墙设置也可以在路由器设置。
网关超时设置:当客户端发送了SYN标志,并且收到了服务端的SYN+ACK后,如果防火墙在计数器到期时仍旧没收到客户端的确认消息,会给服务端发送一个RST包,将当前连接从未连接队列中删去。不过防火墙超时不宜设的过短,因为这样会影响正常连接,也不宜设的过长,因为这样无法有效的防范SYN攻击。
SYN网关:客户端发给服务器一个SYN,服务端收到后返回SYN+ACK,网关在接收到服务端的SYN+ACK后,直接给服务器返回一个ACK确认,这样就从半连接进入到了连接状态,因为服务器的连接队列容纳量往往远大于半连接队列,因此可以有效降低SYN攻击的损害。
SYN代理:当网关代理收到客户端的SYN请求后,代理服务器发送一个SYN+ACK给客户端,如果代理能收到客户端的ACK,那么说明这是一个可用连接,返回给服务端一个ACK建立连接,相当于把SYN攻击的处理交给了代理来做,所以代理必须要有很强的SYN攻击防范能力。
2. 加固TCP/IP协议栈
修改TCP协议,重设最大半连接时间和缩短超时时间,不过这样做可能会影响正常功能的使用。
TCP和UDP的区别
UDP是非连接协议,传输数据之前双方不建立连接,UDP从应用程序那里每拿到一个数据就扔到网络里,在发送端UDP传送数据的速度和应用程序生成数据的速度、计算机的能力以及带宽有关,在接收方,UDP将消息段放入等待队列,应用程序每次从队列中取出一个消息段。
UDP的信息包很短8个字节,而TCP信息包有20个字节。
UDP尽最大努力交付,但不保证可靠交付,因此不需要维护复杂的连接状态表。
UDP是面向报文的,而TCP是面向字节流的,将数据视为一串无结构的字节流。UDP接受应用程序传递下来的报文,添加首部后就向下交付给IP层,既不拆分也不合并,保留报文边界。
ping功能就是典型的UDP传输。
TCP保证数据可靠,UDP可能会丢包,TCP保证数据顺序,UDP不保证。TCP有序列号,确认应答,重发控制,连接控制,窗口控制等。
UDP不进行传输控制,控制应该由应用程序来考虑。
UDP不受拥挤算法控制,即便网络出现拥堵,也不会使得主机发送数据速率降低。
TCP是全双工的。
单工:数据传输只能单向,一条单行道
半双工:一条双行道,只不过同一时间只能进行接受或者发送一种行为
全双工:一条双行道,发送和接受可以同时进行
在网络游戏中啥时候用TCP啥时候用UDP?
对于延迟可容忍的情况,就是可以实现延迟隐藏的情况下可以使用TCP,延迟隐藏指的是在客户端通过一些动画延迟等等,让用户看不到服务器的延迟。但是有些情况对于实时性要求较高,延迟稍微高一点都不行,虽然在PC端看不出来,但是到了移动端,也就是wifi或则3G条件下,很容易发生丢包,由于TCP协议的阻塞机制,导致延迟时间明显变长,我理解的TCP阻塞机制是:服务端发送的数据包客户端没接受到,发生了丢包,然后服务端需要等待一段时间来接收客户端的ACK确认,这段时间是阻塞等待的,也就导致了延迟。解决办法就是自定义可靠的UDP连接。。。
2. socket
socket用来解决网络间的进程通信问题,首先每个进程必须要有个唯一标识,类似于操作系统中的进程标识PID,socket使用ip地址来标识主机,然后用协议+端口来标识主机中的应用程序,也就是利用(ip+协议+端口)来进行网络间的进程标识。
socket来自于Unix的BSD。
socket的相关API
1. int socket(int domain, int type, int protocol)
由于在Unix中一切皆为文件,所以socket的API都是基于文件的,socket函数用于返回一个socket描述符(唯一标识一个socket)。
domain表示协议族,常用的有AF_INET, AF_INET6
type表示socket类型,通常有SOCKET_STREAM, SOCKET_DGRAM
protocol表示协议类型,常用的有IPPROTO_TCP, IPPROTO_UDP
2. int bind(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen)
bind作用是将上面说的网络进程间的唯一标识(协议+ip+端口)绑定给sockfd,其中sockaddr_in结构如下:
struct sockaddr_in { __uint8_t sin_len; sa_family_t sin_family; //协议 in_port_t sin_port; //端口 struct in_addr sin_addr; //IP char sin_zero[8]; };
上面这些参数如果不指定的话,系统会默认分配。
3. int listen(int sockfd, int backlog)
监听客户端的connect请求,backlog指的是socket可以排队的最大连接数。
4. accept(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen)
当客户端向服务器发送connect请求时,服务器监听到这个请求之后会调用accept来接受请求,socket的监听描述符是唯一的,而连接描述符对应每个客户端都有一个,当服务器处理完客户端端请求后,相应的连接描述符会被关闭。
5. recv/send
成功建立连接后,可以进行读写IO的操作了
总结下这两个函数的阻塞和非阻塞模式,首先在阻塞模式下,如果对方没有发送数据,也就是缓冲区是空着的话,recv会保持阻塞状态,一直到有数据可读为止,如果是非阻塞模式,读不到数据会直接返回。对于send,如果是阻塞模式,发送的数据会先放入TCP/IP协议栈的缓冲区中,如果缓冲区空间大于发送数据所占用的空间,那么send可以直接返回,如果缓冲区空间不够,那send就一直阻塞,等到对端接收部分数据后,缓冲区空间足够了,再写入返回,如果是非阻塞模式,那么不管缓冲区够不够,能写入多少就写入多少,然后直接返回了。
如果使用IO多路复用监听读写事件的话,只要缓冲区不满,send就可以一直发送数据(一直可写),而recv只有在缓冲区有消息的时候才能读,还有一种特殊情况,在其他语言还没有尝试过,但是在python中,如果对端close掉,但是没有shutdown,这时只是发生了一个假关闭,其实连接还在,这时候依旧可以send,而且也可以recv,即便对端关闭前什么数据都没发送,一般用IO多路复用监听,对端没数据发送过来的时候是不可读的,但是对端关闭了,就可读了,但是recv不到数据,就得到一个空字符串,目前不知道为啥。。
6. close
完成了所有操作之后要关闭socket描述符。
socket中TCP连接三次握手和结束四次握手
SYN 建立连接 同步序列号 TCP建立连接时将SYN置为1
ACK 响应连接 确认应答 确认号为X表示前X-1个数据都接受到了,ACK=1时确认号才有效,ACK=0时,表示确认号无效,需要重传数据,确保数据的完整性。
FIN 结束连接 当TCP完成数据传输将连接断开时,提出断开的一方将这一位置1
1. 三次握手连接
第一次握手:客户端向服务器发送connect请求,发送一个SYN=j的数据包,此时客户端的connect进入阻塞状态,表示客户端发起连接请求,服务端可以用序列号SYN=j作为起始数据段来回应。
第二次握手:服务端监听到连接请求,收到SYN=j的数据包之后,调用accept函数接受客户端connect请求,并向客户端发送确认ACK=j+1(表示确认接受到了客户端的SYN,如果没收到的话需要重发)和SYN=k的数据包(表示同意连接,可以进行连接了),此时accept一直都处于阻塞状态,表示服务端接受了客户端的连接请求,客户端可以用序列号SYN=k作为起始数据段回应。
第三次握手:客户端接收到SYN=k,ACK=j+1的数据包,connect取消阻塞状态返回,再向服务器发送ACK=k+1进行确认,当服务器收到ACK时,accept阻塞状态返回,成功建立连接。
2. 取消连接四次握手
第一次握手:客户端向服务端发送FIN=m,表示数据发送完毕,请求关闭连接。
第二次握手:服务端接收到FIN=m后,对FIN进行确认,发送ACK=m+1,表示收到了客户端的关闭连接请求,但这个时候服务端的数据可能还没发送完毕,所以不能立即关闭。
第三次握手:当服务端数据发送完毕后,发送FIN=n,表示可以关闭连接了。
第四次握手:客户端接收到FIN后进行确认,发送ACK=n+1,双方连接关闭。
基于TCP的socket服务端和客户端通信流程大致为:
1. 服务端:socket->bind->listen->accept->recv->send->close
2. 客户端:socket->connect->send->recv->close
为什么建立连接需要三次握手,而终止连接需要四次?
在建立连接的过程中,服务器在收到connect请求后,将SYN和ACK同时发送给客户端,而终止连接的过程中,服务器收到客户端的FIN表示客户端已经没有数据要发送了,但是这时服务器可能还在向客户端发送数据,因此在返回ACK后不能立即发送FIN,等数据全都发送完之后再发送一个FIN给客户端表示可以关闭连接了。
TCP socket 最简单的一对一C/S代码
服务端
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <unistd.h> const int DEFAULT_PORT = 8000; const int MAXLINE = 4096; int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; char buff[4096]; int n; //初始化socket if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("Create Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } //绑定socket,指定了协议族、IP地址和端口 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(DEFAULT_PORT); if(bind(socket_fd, (struct sockaddr*)&(servaddr), sizeof(servaddr)) == -1) { printf("Bind Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } //创建监听 if(listen(socket_fd, 10) == -1) { printf("Listen Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } printf("+++++++++++++++ Waiting For Client Request +++++++++++++++\n"); while(1) { //accept连接请求,调用listen后这里是阻塞的,直到有客户端发送connect请求 if((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1) { printf("Connect Socket Error: %s(errno: %d)\n", strerror(errno), errno); continue; } //获取客户端消息 n = recv(connect_fd, buff, MAXLINE, 0); //fork一个子进程 if(!fork()) { char *s = "Hello, you are connected!"; //向客户端发送消息 if(send(connect_fd, s, strlen(s), 0) == -1) perror("Send Error"); close(connect_fd); exit(0); } buff[n]='\0'; printf("Receive Message From Client: %s\n", buff); close(connect_fd); } close(socket_fd); return 0; }
客户端
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <arpa/inet.h> #include <unistd.h> const int MAXLINE = 4096; int main(int argc, char** argv) { int socket_fd, n; char sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; if(argc != 2) { printf("usage: ./client <ipaddress>\n"); exit(0); } //建立socket if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("Create Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } //向服务端发起连接请求 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { printf("Inet_pton Error for %s\n", argv[1]); exit(0); } if(connect(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { printf("Connect Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } printf("Send Message to Server: \n"); //向服务端发送消息 fgets(sendline, 4096, stdin); if(send(socket_fd, sendline, sizeof(sendline), 0) < 0) { printf("Send Message Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } //接受服务端发来的消息 if ((n=recv(socket_fd, buf, MAXLINE, 0)) == -1) { perror("Receive Error"); exit(1); } buf[n]='\0'; printf("Receive: %s\n", buf); close(socket_fd); return 0; }
UDP Socket
由于UDP不是面向连接的,因此不存在三次握手,客户端不会发送connect请求,服务端也就不需要listern监听以及accept就收请求,基本流程就是:
服务端:socket->bind->recvfrom->sendto->close
客户端:socket->sendto->recvfrom->close
注意这里recvfrom必须指定发送端的协议族,sendto必须指定接收段段协议族,不能使用NULL。
服务端:
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <unistd.h> const int DEFAULT_PORT = 8000; const int MAXLINE = 4096; int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; struct sockaddr_in clientaddr; char buff[4096]; int n; //初始化socket if((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { printf("Create Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } //创建服务器和客户端的协议族 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(DEFAULT_PORT); memset(&clientaddr, 0, sizeof(clientaddr)); clientaddr.sin_family = AF_INET; clientaddr.sin_addr.s_addr = htonl(INADDR_ANY); clientaddr.sin_port = htons(DEFAULT_PORT); //绑定socket,指定了协议族、IP地址和端口 if(bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("Bind Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } printf("+++++++++++++++ Waiting For Client Request +++++++++++++++\n"); socklen_t len = sizeof(clientaddr); //从客户端接收数据 if((n = recvfrom(socket_fd, buff, MAXLINE, 0, (struct sockaddr*)&clientaddr, &len)) == -1) { perror("Received Failed"); exit(0); } char *s = "Hello, you are connected!"; //向客户端发送数据 if(sendto(socket_fd, s, MAXLINE, 0, (struct sockaddr*)&clientaddr, sizeof(clientaddr)) == -1) perror("Send Error"); buff[n]='\0'; printf("Receive Message From Client: %s\n", buff); close(socket_fd); return 0; }
客户端
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <arpa/inet.h> #include <unistd.h> const int MAXLINE = 4096; int main(int argc, char** argv) { int socket_fd, n; char sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; //建立socket if((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { printf("Create Socket Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } //初始化服务端协议族 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); printf("Send Message to Server: \n"); //向服务端发送消息 fgets(sendline, 4096, stdin); if(sendto(socket_fd, sendline, MAXLINE, 0, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("Send Message Error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } socklen_t len = sizeof(servaddr); //接收服务端发来的消息 if ((n=recvfrom(socket_fd, buf, MAXLINE, 0, (struct sockaddr*)&servaddr, &len)) == -1) { perror("Receive Error"); exit(1); } buf[n]='\0'; printf("Receive: %s\n", buf); close(socket_fd); return 0; }
并发模型
1. 采用select实现IO多路复用
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout)
nfds 表示文件描述符的个数,一般用最大的文件描述符加1
readfds 指向等待可读性检查的套接字接口描述符的指针,为NULL表示不进行可读性检查
writefds 指向等待可写性检查的套接字接口描述符的指针,为NULL表示不进行可写性检查
exceptfds 指向等待错误检查的套接字接口描述符的指针,为NULL表示不进行错误检查
timeout 每次检查需要阻塞多长时间,为NULL表示无阻塞模式
几个宏:
FD_ZERO(fd_set* set)表示将文件描述符集合清空
FD_SET(int fd, fd_set* set)表示将fd加入文件描述符集合set中
FD_ISSET(int fd, fd_set* set)表示检查集合中fd的状态是否发生变化
FD_CLR(int fd, fd_set* set)表示将fd从set中删除
使用select函数的客户端流程大致为:先建立socket,然后bind协议族,之后开始listen端口,然后进入死循环,首先清理fd_set并重新装填(貌似因为select函数会修改集合),然后通过select进行一次查看,select返回小于0表示select错误,select等于0表示select超时,对于select大于0的情况,检查fd_set集合中的所有fd,如果有客户端发送了connect请求,那么fd_set中管理的服务端socket描述符状态会发生变化,执行accept函数接收请求,这样一来就解决了一直进行等待connect使得accept而发生阻塞的问题,对于已经accept的客户端请求,通过accept返回的客户端描述符来进行管理,当这些客户端描述符状态改变时,可以通过send或者recv来进行读写操作,这样一来就避免了send或者recv一直等待而导致的阻塞,注意这里的send和recv第一个参数都是客户端描述符,这里和客户端写法不同,客户端的send和recv第一个参数都是自己的socket描述符,我的理解是因为,服务端是一对多,对于监听connect请求要通过自己的socket描述符来管理,而对于建立连接后的客户端读写请求必须通过accept返回的各个客户端描述符来进行管理(其实就是管理多个文件),对于客户端来说各种请求都是一对一的,所以select管理的都是用自己的socket描述符(只需要管理一个文件)。
这个代码目前并不完善,只是参考下select流程就好,服务端:
#include <stdio.h> #include <sys/socket.h> #include <stdlib.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <sys/select.h> #include <sys/types.h> #include <sys/times.h> #include <string> using namespace std; const int BACKLOG = 5; const int MAXCOCURRENT = 10; const int SERVER_PORT = 8000; const int BUFFER_SIZE = 1024; const char *QUIT_CMD = ".quit"; int client_fds[MAXCOCURRENT]; int main(int argc, const char* argv[]) { char input_msg[BUFFER_SIZE]; char recv_msg[BUFFER_SIZE]; struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(server_addr.sin_zero), 8); int server_sock_fd; if((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("Create Socket Error"); exit(0); } if(bind(server_sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("Connect Error"); exit(0); } if(listen(server_sock_fd, BACKLOG) == -1) { perror("Listen Error"); exit(0); } printf("服务器已启动!\n"); fd_set server_fd_set; int max_fd = -1; struct timeval tv; while(1) { tv.tv_sec = 10; tv.tv_usec = 0; FD_ZERO(&server_fd_set); FD_SET(STDIN_FILENO, &server_fd_set); if(max_fd < STDIN_FILENO) max_fd = STDIN_FILENO; FD_SET(server_sock_fd, &server_fd_set); if(max_fd < server_sock_fd) max_fd = server_sock_fd; for(int i=0;i<MAXCOCURRENT;++i) { if(client_fds[i]!=0) { FD_SET(client_fds[i], &server_fd_set); if(max_fd < client_fds[i]) max_fd = client_fds[i]; } } int select_ret; if((select_ret = select(max_fd + 1, &server_fd_set, NULL, NULL, &tv)) < 0) { perror("Select Error"); continue; } else if(select_ret == 0) { printf("Select Timeout."); continue; } else { if(FD_ISSET(STDIN_FILENO, &server_fd_set)) { printf("发送消息:\n"); bzero(input_msg, BUFFER_SIZE); fgets(input_msg, BUFFER_SIZE, stdin); if(strcmp(input_msg, QUIT_CMD) == 0) exit(0); for(int i=0;i<MAXCOCURRENT;++i) { if(client_fds[i]) { printf("client_fds[%d] = %d\n",i, client_fds[i]); send(client_fds[i], input_msg, BUFFER_SIZE, 0); } } } if(FD_ISSET(server_sock_fd, &server_fd_set)) { struct sockaddr_in client_addr; socklen_t client_addr_len; int client_sock_fd = accept(server_sock_fd, (struct sockaddr*)&client_addr, &client_addr_len); printf("New connection client_sock_fd = %d\n",client_sock_fd); if(client_sock_fd > 0) { int index = -1; for(int i=0;i<MAXCOCURRENT;++i) { if(client_fds[i] == 0) { index = i; client_fds[i] = client_sock_fd; break; } } if(index != -1) { printf("新客户端%d加入成功: %s:%d\n", client_fds[index], inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } else { bzero(input_msg, BUFFER_SIZE); strcpy(input_msg, "服务器加入的客户端数已达到最大,无法加入!\n"); send(client_sock_fd, input_msg, BUFFER_SIZE, 0); printf("客户端连接达到最大值,新客户端加入失败: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } } } for(int i=0;i<MAXCOCURRENT;++i) { if(client_fds[i]) { if(FD_ISSET(client_fds[i], &server_fd_set)) { bzero(recv_msg, BUFFER_SIZE); int byte_num = recv(client_fds[i], recv_msg, BUFFER_SIZE, 0); printf("byte=%d\n",byte_num); if(byte_num > 0) { if(byte_num > BUFFER_SIZE) byte_num = BUFFER_SIZE; recv_msg[byte_num] = '\0'; // printf("客户端(%d): %s\n", client_fds[i], recv_msg); string s = "客户端"; s += (int)client_fds[i]; s += recv_msg; for(int j=0;j<MAXCOCURRENT;++j) { if(client_fds[j]&&i!=j) { send(client_fds[j], s.c_str(), BUFFER_SIZE, 0); } } } else if(byte_num < 0) { printf("从客户端(%d)接收消息出错.\n", client_fds[i]); } else { printf("客户端(%d)退出了!\n", client_fds[i]); FD_CLR(client_fds[i], &server_fd_set); client_fds[i] = 0; } } } } } } return 0; }
客户端:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/select.h> #include <sys/times.h> const int BUFFER_SIZE = 4096; int main(int argc, char *argv[]) { struct sockaddr_in server_addr; server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8000); bzero(&(server_addr.sin_zero), 8); int server_sock_fd; if((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("Create Socket Error"); exit(0); } char recv_msg[BUFFER_SIZE]; char input_msg[BUFFER_SIZE]; if(connect(server_sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) { perror("Connect Failed"); exit(0); } printf("已与服务器建立连接!\n"); fd_set client_fd_set; struct timeval tv; while(1) { tv.tv_sec = 10; tv.tv_usec = 0; FD_ZERO(&client_fd_set); FD_SET(STDIN_FILENO, &client_fd_set); FD_SET(server_sock_fd, &client_fd_set); select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv); if(FD_ISSET(STDIN_FILENO, &client_fd_set)) { bzero(input_msg, BUFFER_SIZE); fgets(input_msg, BUFFER_SIZE, stdin); if(send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1) { perror("发送消息出错"); } } if(FD_ISSET(server_sock_fd, &client_fd_set)) { bzero(recv_msg, BUFFER_SIZE); int byte_num = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0); if(byte_num < 0) { printf("接收消息出错!\n"); } else if(byte_num == 0) { printf("服务器端退出!\n"); } else { if(byte_num > BUFFER_SIZE) byte_num = BUFFER_SIZE; recv_msg[byte_num] = '\0'; printf("服务器: %s\n", recv_msg); } } } return 0; }
2. 采用poll实现IO多路复用
int poll(struct pollfd*, nfds_t, int)
pollfd是描述符集合,nfds_t是集合大小,int是阻塞时间
poll和select的区别是没有连接数的限制,并且不用清空描述符集合,select函数每次执行完之后会将fd_set改变,所以要清空重新添加,poll不需要这样做,因此对于多个客户端连接,poll的效率更高。
poll的描述符集合
struct pollfd
{
int fd;
short events;
short revents;
};
events表示你所关心的该描述符某种状态的变化,比如POLLIN表示有数据可读。
revents表示poll函数检测到该描述符上实际的状态变化。
用revents&POLLIN==POLLIN表示该描述符可读。
使用poll函数的服务端大致执行流程是:建立socket,bind绑定,listen监听,初始化pollfd集合,死循环执行poll,检查pollfd中的每个描述符的revents,判断是否可以进行读写操作。对于服务端的描述符如果revents == POLLIN,表示有connect请求,进行accept,将新连接的客户端描述符加入到pollfd集合中,如果客户端描述符revents==POLLIN,表示有客户端发送消息过来,通过recv获取。总的来说和select相比就是少了一个清理并重新装填的步骤。
代码目前bug多多,仅供流程参考,服务端:
#include <stdio.h> #include <sys/socket.h> #include <stdlib.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <sys/poll.h> #include <sys/types.h> #include <sys/times.h> #include <string> using namespace std; const int BACKLOG = 5; const int MAXCOCURRENT = 10; const int SERVER_PORT = 8000; const int BUFFER_SIZE = 1024; const char *QUIT_CMD = ".quit"; int client_fds[MAXCOCURRENT]; int main(int argc, const char* argv[]) { char input_msg[BUFFER_SIZE]; char recv_msg[BUFFER_SIZE]; struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(server_addr.sin_zero), 8); int server_sock_fd; if((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("Create Socket Error"); exit(0); } if(bind(server_sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("Connect Error"); exit(0); } if(listen(server_sock_fd, BACKLOG) == -1) { perror("Listen Error"); exit(0); } printf("服务器已启动!\n"); struct pollfd client_fds[MAXCOCURRENT]; client_fds[0].events = POLLIN; client_fds[0].fd = server_sock_fd; int max_fd = server_sock_fd + 1; for(int i=1;i<=MAXCOCURRENT;++i) client_fds[i].fd = -1; while(1) { int poll_ret = poll(client_fds, max_fd, 10); if(poll_ret == -1) { perror("Poll Error"); continue; } if((client_fds[0].revents & POLLIN) == POLLIN) { int clientfd = accept(server_sock_fd, (struct sockaddr*)NULL, NULL); if(clientfd == -1) { perror("Connect Error"); continue; } for(int i=1;i<MAXCOCURRENT;++i) { if(client_fds[i].fd == -1) { client_fds[i].fd = clientfd; client_fds[i].events = POLLIN; if(clientfd > max_fd) max_fd = clientfd + 1; break; } } } for(int i=1;i<MAXCOCURRENT;++i) { if(client_fds[i].fd == -1) continue; if((client_fds[i].revents & POLLIN) == POLLIN) { int byte_num = read(client_fds[i].fd, recv_msg, BUFFER_SIZE); if(byte_num == -1) { perror("Read Error"); continue; } if(byte_num == 0) { printf("Client Close"); close(client_fds[i].fd); client_fds[i].fd = -1; continue; } recv_msg[byte_num] = '\0'; sprintf(input_msg, "客户端 %d 说: %s\n", i, recv_msg); for(int j=0;j<MAXCOCURRENT;++j) if(client_fds[j].fd !=-1 && client_fds[j].fd != client_fds[i].fd) send(client_fds[j].fd, input_msg, BUFFER_SIZE, 0); } } } return 0; }
3. 采用epoll实现IO多路复用
select和poll每次调用的时候都会遍历整个文件描述符集合,导致效率呈线性下降,而epoll每次只会对处于活动状态的socket进行扫描,这主要是因为epoll是根据每个fd上的callback回调函数来实现的,所以只有活动的socket才会去调用epoll
int epoll_create(int size) size表示监听文件连接数有多大,和select和poll函数传入最大文件描述符+1不同,这里只要传入数量,每个epoll_create函数调用都会占用一个fd,因此再使用完后要将其close掉,否则可能会导致fd耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 第一个参数表示epoll文件描述符,第二个参数表示动作,用宏来表示
EPOLL_CTL_ADD 注册新的fd到epfd中
EPOLL_CTL_MOD 修改epfd中某个fd的监听事件
EPOLL_CTL_DEL 删除epfd中的某个fd
第三个参数表示要操作的文件描述符,第四个参数表示要监听的这个文件描述符的事件。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
代码如下:
#include<iostream> #include<stdio.h> #include<sys/epoll.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<string.h> #include<unistd.h> using namespace std; const int buffer_size = 1024; const int max_events = 1024; int main() { int server_sockfd; int client_sockfd; int len; struct sockaddr_in server_addr; struct sockaddr_in client_addr; char buf[buffer_size]; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family=AF_INET; server_addr.sin_addr.s_addr=INADDR_ANY; server_addr.sin_port=htons(8002); socklen_t sin_size=sizeof(client_addr); if((server_sockfd=socket(AF_INET, SOCK_STREAM, 0))<0) { perror("create socket error"); exit(0); } if(bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))<0) { perror("bind error"); exit(0); } listen(server_sockfd, 5); int epoll_fd; epoll_fd=epoll_create(max_events); if(epoll_fd==-1) { perror("create epoll error"); exit(0); } struct epoll_event ev; struct epoll_event events[max_events]; ev.events=EPOLLIN; ev.data.fd=server_sockfd; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_sockfd, &ev)==-1) { perror("epoll control error"); exit(0); } int nfds; cout<<"Server is waiting for connect!"<<endl; while(1) { nfds=epoll_wait(epoll_fd, events, max_events, 3); if(nfds==-1) { perror("start epoll wait error"); exit(0); } for(int i=0;i<nfds;++i) { if(events[i].data.fd==server_sockfd) { if((client_sockfd=accept(server_sockfd, (struct sockaddr*)&client_addr, &sin_size))<0) { perror("connect client error"); exit(0); } ev.events=EPOLLIN; ev.data.fd=client_sockfd; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_sockfd, &ev)==-1) { perror("epoll control error"); exit(0); } } else { len=recv(events[i].data.fd, buf, buffer_size, 0); if(len<0) { perror("receive client error"); exit(0); } buf[len]='\0'; cout<<buf<<endl; send(events[i].data.fd, "Server has received your message.\n", buffer_size, 0); } } } close(epoll_fd); close(server_sockfd); return 0; }
#include<iostream> #include<stdio.h> #include<sys/epoll.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<string.h> #include<unistd.h> using namespace std; const int buffer_size=1024; int main() { int client_sockfd; struct sockaddr_in server_addr; char buf[buffer_size]; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family=AF_INET; server_addr.sin_addr.s_addr=inet_addr("127.0.0.1"); server_addr.sin_port=htons(8002); if((client_sockfd=socket(AF_INET, SOCK_STREAM, 0))<0) { perror("create socket error"); exit(0); } if(connect(client_sockfd, (struct sockaddr*)&server_addr, sizeof(sockaddr))<0) { perror("connect error"); exit(0); } while(1) { cout<<"Input message:"<<endl; scanf("%s",buf); send(client_sockfd, buf, buffer_size, 0); int n=recv(client_sockfd, buf, buffer_size, 0); if(n<0) { perror("receive error"); continue; } printf("Receive from Server: %s\n",buf); } close(client_sockfd); return 0; }
4. 采用进程并发
5. 采用线程并发