注意:这属于三次握手中的第一次握手,目的是告诉S端我C端的初始序列号SEQ的值
(2)S端接收到该SYN消息后,发送ACK作为回复,同时附带序列号SEQ为N;注意:这属于三次握手中第二次握手的第一个包,表示S端已经知道了C端的初始SEQ值为N
(3)S端在发出第一个ACK包之后,还会发送一个SYN握手消息,同时附带初始序列号SEQ为M - 1;注意:这属于三次握手中第二次握手的第二个包,目的是告诉C端我S端的初始序列号SEQ的值
(4)C端收到SYN消息后,发送ACK作为回复,同时附带序列号为M;注意:这属于三次握手中第三次握手,此时通信双方彼此确认了对方的数据包初始序列号,三次握手完成
(5)通信双方开始进行数据传输; ### 2. SYN Flood攻击 如图右侧所示,当完成了三次握手的第二步时,该连接在S端被认为属于半连接状态。此时,连接的信息会被保存在S端的半连接池当中。一旦S端收到了第4个包,如果S端能在半连接池里找到对应的连接信息则说明三次握手完成。 由于S端半连接池的存在,因此存在一种叫做半连接洪水的网络攻击(SYN Flood)。具体而言是**攻击者使用大量的“肉鸡”向S端发送第一次握手消息,而不发送第三次握手消息,进而导致S端的半连接池被占满**。于是S端便不能再响应更多的连接请求。 S端应对SYN Flood攻击的常见办法: (1)对连接池中的信息设置生命周期,比如1s,在生命周期内没有收到第三次握手消息则将其剔除; (2)加大半连接池的容量; (3)“拉黑”只发送第一次握手消息的IP地址; 但实际上以上的方法全是治标不治本,想要彻底解决SYN Flood攻击,**唯一办法是弃用半连接池**。半连接池没了,你还怎么攻击? 如果不用半连接池,那应该如何保证S端收到ACK回复时能够确认该ACK对应的连接确实完成了三次握手的前面两步呢?答案是**用cookie**。具体如下: (1)S端收到第一次握手后,使用下面的公式计算一个哈希值作为cookie发送给C端; ``` hash((Cip + Cport + Sip + Sport + proto) | salt) ``` 即:使用C端的ip、C端的port、S端的ip、S端的port以及使用的协议proto之和或上盐值salt,对该结果取哈希值作为cookie发送给C端。 (2)要求C端将收到的cookie值在第三次握手中发送回来,S端使用当前的盐值salt利用相同的公式计算,看是否和cookie匹配。如果匹配则三次握手成功,否则不成功 注意:盐值salt是由内核产生的,一秒钟变一次。S端如果使用当前的salt计算出来结果不匹配,则用上一秒的salt值继续匹配。如果还匹配不上,说明这个第三次握手消息可能已经超时了。 ## P254 ~ P263 进程间通信 - 流式套接字详解 流式套接字(TCP)的特点: (1)能保证收到的消息和消息内容是正确的; (2)能保证点对点的通信形式; ### 1. 流式套接字通信步骤 TCP通信的步骤: **C端(主动端)**: (1)获取socket (2)给socket取得地址(可省略) (3)发送连接 (4)收 / 发消息 (5)关闭 **S端(被动端)**: (1)获取socket (2)给socket取得地址 (3)设置socket为监听模式 (4)接收连接 (5)收 / 发消息 (6)关闭 ### 2. 基于流式套接字的echo服务器实现 [完整源码](https://github.com/wallace-lai/learn-apue/tree/main/src/ipc/socket/stream/basic) 我们使用TCP实现一个简单的echo服务器,即当C端发送请求时,S端返回当前的时间戳。 **协议**: ```c #define SERVER_PORT "1989" #define FORMAT_STAMP "Recv server stamp : %lld\r\n" ``` 解释: (1)协议侧只需要约定S端的端口号以及S端返回的消息格式即可 **客户端**: ```c if (argc < 2) { /* ... */ } int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { /* ... */ } // bind(); ``` 解释: (1)发送ip由命令行传入 (2)创建socket后,C端的bind操作可以省略,让操作系统自动分配可用端口号 ```c struct sockaddr_in raddr; raddr.sin_family = AF_INET; raddr.sin_port = htons(atoi(SERVER_PORT)); inet_pton(AF_INET, argv[1], (void *)&raddr.sin_addr); ret = connect(sock, (void *)&raddr, sizeof(raddr)); if (ret < 0) { /* ... */ } ``` 解释: (1)使用connect和S端建立连接,需要给定S端的端口号和ip地址(由命令行传入) ```c static char buffer[STAMPSIZE]; memset(buffer, 0, STAMPSIZE); // 1. use recv // ssize_t len = recv(sock, buffer, STAMPSIZE, 0); // if (len < 0) { /* ... */ } // printf("%s\r\n", buffer); // 2. use io FILE *fp = fdopen(sock, "r"); if (fp == NULL) { /* ... */ } if (fgets(buffer, STAMPSIZE, fp) != NULL) { printf("%s\r\n", buffer); } fclose(fp); ``` 解释: (1)C端接收S端发来的数据有两种方式,一种是使用recv接口 (2)另一种是贯彻“一切皆文件”的思想,使用io接口来读取描述符中的内容 **服务端**: ```c int sock = socket(AF_INET, SOCK_STREAM, 0 /* IPPROTO_TCP, IPPROTO_SCTP */); if (sock < 0) { /* ... */ } int val = 1; ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); if (ret < 0) { /* ... */ } struct sockaddr_in laddr; laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVER_PORT)); inet_pton(AF_INET, "0.0.0.0", (void *)&laddr.sin_addr); ret = bind(sock, (void *)&laddr, sizeof(laddr)); if (ret < 0) { /* ... */ } ``` 解释: (1)服务端首先取得socket (2)设置sock为SO_REUSEADDR,这是为了在S端程序关闭后立即重新启动时bind操作不出错 (3)使用bind绑定S端的ip和端口号 ```c ret = listen(sock, 200); if (ret < 0) { /* ... */ } struct sockaddr_in raddr; socklen_t raddr_len = 0; static char ipstr[IPSTRSIZE]; while (1) { ret = accept(sock, (void *)&raddr, &raddr_len); if (ret < 0) { /* ... */ } memset(ipstr, 0, IPSTRSIZE); inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE); printf("Recv connection from client (%s:%d).\n", ipstr, ntohs(raddr.sin_port)); server_execute(ret); close(ret); } ``` 解释: (1)S端使用listen监听C端的连接请求 (2)S端一旦和C端的连接建立,则accept会返回C端的addr信息以及文件描述符ret (3)S端打印C端的ip和端口号,随后执行server_execute操作 ```c static void server_execute(int fd) { static char buffer[STAMPSIZE]; memset(buffer, 0, STAMPSIZE); int len = sprintf(buffer, FORMAT_STAMP, (long long)time(NULL)); int ret = send(fd, buffer, len, 0); if (ret < 0) { perror("send()"); exit(1); } } ``` 解释: (1)S端要做的很简单,往C端的文件描述符中写入本地的时间戳即可 (2)S端调用send将时间戳发送给C端 ### 3. 基于流式套接字的echo服务器的并发版本 我们先做一个简单的版本,即在S端改成多进程并发的版本,如下所示: ```c while (1) { ret = accept(sock, (void *)&raddr, &raddr_len); if (ret < 0) { /* ... */ } pid_t pid = fork(); if (pid < 0) { /* ... */ } if (pid == 0) { // child memset(ipstr, 0, IPSTRSIZE); inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE); printf("Recv connection from client (%s:%d).\n", ipstr, ntohs(raddr.sin_port)); server_execute(ret); close(ret); exit(0); } } ``` 解释: (1)使用fork创建子进程,在子进程中执行server_execute即可 但是上述的多进程并发版本存在几个问题: (1)系统中支持的进程数量(pid_t)是有限的 (2)为每一个客户端开辟一个进程所带来的开销巨大 因此,只能限定子进程的个数N,变成了N个子进程去抢任务执行。但是,N个子进程去抢任务又会遇到进程同步的问题,又回到了之前讲过的并发的话题。 ### 4. 基于流式套接字的图片下载功能实现 [完整源码](https://github.com/wallace-lai/learn-apue/blob/main/src/ipc/socket/stream/webdl.c) 在本地搭建web服务器,然后使用HTTP协议下载服务器中的图片。只需要复用C端代码然后稍微改动即可。 ```c struct sockaddr_in raddr; raddr.sin_family = AF_INET; raddr.sin_port = htons(80); inet_pton(AF_INET, argv[1], (void *)&raddr.sin_addr); ret = connect(sock, (void *)&raddr, sizeof(raddr)); if (ret < 0) { perror("connect()"); exit(1); } ``` 解释: (1)此时C端要连接的端口号是80,即HTTP协议所在端口号 (2)然后调用connect与S端(apache web服务器)建立连接 ```c // open socket file descriptor FILE *fp = fdopen(sock, "r+"); if (fp == NULL) { perror("fdopen()"); exit(1); } // write http request to socket fprintf(fp, "GET /logo.jpg\r\n\r\n"); fflush(fp); // recv response data from socket size_t len = 0; static char buffer[BUFSIZ]; while (1) { len = fread(buffer, 1, BUFSIZ, fp); if (len <= 0) { break; } fwrite(buffer, 1, len, stdout); } ``` 解释: (1)连接建立后,打开socket文件描述符获取文件指针 (2)往文件中写入GET请求,然后刷新缓冲区,此时便完成了http request的发送 (3)随后C端不停地从socket中读取S端的发送的数据往标准输出打印 ### 5. 静态进程池套接字的echo服务器实现 [完整源码](https://github.com/wallace-lai/learn-apue/tree/main/src/ipc/socket/stream/pool_static) 在本案例中,要求在S端设立数量固定的进程池来处理来自C端的请求。我们只需要在echo服务器的基本版本上改进S端代码即可。 ```c #define PROCNUM 4 int sock = socket(AF_INET, SOCK_STREAM, 0 /* IPPROTO_TCP, IPPROTO_SCTP */); if (sock < 0) { /* ... */ } int val = 1; ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); if (ret < 0) { /* ... */ } struct sockaddr_in laddr; laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVER_PORT)); inet_pton(AF_INET, "0.0.0.0", (void *)&laddr.sin_addr); ret = bind(sock, (void *)&laddr, sizeof(laddr)); if (ret < 0) { /* ... */ } ret = listen(sock, 200); if (ret < 0) { /* ... */ } ``` 解释: (1)限定子进程个数为4个 (2)父进程同样先创建socket、绑定端口、开始监听 ```c for (int i = 0; i < PROCNUM; i++) { pid_t pid = fork(); if (pid < 0) { /* ... */ } if (pid == 0) { server_loop(sock); exit(0); } } for (int i = 0; i < PROCNUM; i++) { wait(NULL); } close(sock); exit(0); ``` 解释: (1)父进程创建4个子进程 (2)每个子进程执行server_loop操作,该操作实际上是一个死循环不会自动停止,因此子进程的exit(0)不会执行 (3)父进程回收4个子进程的资源,最后关闭socket结束程序 ```c static void server_execute(int fd) { static char buffer[STAMPSIZE]; memset(buffer, 0, STAMPSIZE); int len = sprintf(buffer, FORMAT_STAMP, (long long)time(NULL)); int ret = send(fd, buffer, len, 0); if (ret < 0) { /* ... */ } } static void server_loop(int sock) { int ret; struct sockaddr_in raddr; socklen_t raddr_len = 0; static char ipstr[IPSTRSIZE]; while (1) { // accept can be locked automatically ret = accept(sock, (void *)&raddr, &raddr_len); if (ret < 0) { /* ... */ } memset(ipstr, 0, IPSTRSIZE); inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE); printf("[%d] Recv connection from client (%s:%d).\n", getpid(), ipstr, ntohs(raddr.sin_port)); server_execute(ret); close(ret); } } ``` 解释: (1)子进程不断地调用accept接受C端的连接,随后向C端发送本机的时间戳 (2)注意accept自身是可重入的,无需设置额外的互斥锁,就能保证4个子进程有序地accept同一个socket 静态进程池(固定子进程个数)的弊端: (1)无法应对瞬时的大流量冲击 (2)S端闲时,子进程也不销毁,仍然占用系统资源 可以将静态进程池改造成动态进程池,要求池中子进程数量不少于下限N(比如10)个,不超过上限M(比如200)个。 ### 6. 动态进程池套接字的echo服务器实现 [完整源码](https://github.com/wallace-lai/learn-apue/tree/main/src/ipc/socket/stream/pool_dynamic) 解释:等代码重构后再写 ## P264 ~ P266 进程间通信 - anytimer的实现 暂缓