计算机网络

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. 采用线程并发

 

posted @ 2016-03-19 19:14  wwwsealss  阅读(1690)  评论(0编辑  收藏  举报