├── README.md ├── ch01 ├── README.md ├── fd_seri.c ├── hello_client.c ├── hello_server.c ├── low_open.c └── low_read.c ├── ch02 ├── README.md ├── tcp_client.c └── tcp_server.c ├── ch03 ├── README.md ├── endian_conv.c ├── inet_addr.c ├── inet_aton.c └── inet_ntoa.c ├── ch04 ├── README.md ├── echo_client.c ├── echo_server.c ├── hello_client.c └── hello_server.c ├── ch05 ├── My_op_client.c ├── My_op_server.c ├── README.md ├── echo_client2.c ├── homework │ ├── tcp_client_kehou5.c │ ├── tcp_client_kehou6.c │ ├── tcp_server_kehou5.c │ └── tcp_server_kehou6.c ├── op_client.c └── op_server.c ├── ch06 ├── README.md ├── bound_host1.c ├── bound_host2.c ├── homework │ ├── uchar_client.c │ └── uchar_server.c ├── uecho_client.c ├── uecho_con_client.c └── uecho_server.c ├── ch07 ├── README.md ├── file_client.c └── file_server.c ├── ch08 ├── README.md ├── gethostbyaddr.c └── gethostbyname.c ├── ch09 ├── README.md ├── get_buf.c ├── reuseadr_eserver.c ├── set_buf.c └── sock_type.c ├── ch10 ├── README.md ├── echo_mpclient.c ├── echo_mpserv.c ├── fork.c ├── homework │ ├── kehou3.c │ └── kehou5.c ├── remove_zomebie.c ├── sigaction.c ├── signal.c ├── test_server.c ├── wait.c ├── waitpid.c └── zombie.c ├── ch11 ├── README.md ├── echo_storeserv.c ├── homework │ └── kehou4.c ├── pipe1.c ├── pipe2.c └── pipe3.c ├── ch12 ├── README.md ├── echo_selectserv.c └── select.c ├── ch13 ├── README.md ├── oob_recv.c ├── oob_send.c ├── peek_recv.c ├── peek_send.c ├── readv.c └── writev.c ├── ch14 ├── README.md ├── news.txt ├── news_receiver.c ├── news_receiver_brd.c ├── news_sender.c └── news_sender_brd.c ├── ch15 ├── README.md ├── desto.c ├── echo_client.c ├── echo_stdserv.c ├── stdcpy.c ├── syscpy.c └── todes.c ├── ch16 ├── README.md ├── dup.c ├── sep_clnt.c ├── sep_serv.c └── sep_serv2.c ├── ch17 ├── README.md ├── echo_EDGEserv.c ├── echo_EPETserv.c ├── echo_EPLTserv.c ├── echo_epollserv.c └── homework │ ├── char_EPETserv.c │ ├── char_EPLTserv.c │ └── chat_clnt.c ├── ch18 ├── README.md ├── chat_clnt.c ├── chat_server.c ├── homework │ ├── echo_client.c │ └── echo_threadserv.c ├── mutex.c ├── semaphore.c ├── thread1.c ├── thread2.c ├── thread3.c └── thread4.c └── ch24 ├── README.md ├── index.html └── webserv_linux.c /ch01/README.md: -------------------------------------------------------------------------------- 1 | ## 第一章:理解网络编程和套接字 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点连接可能进不去。 4 | 5 | ### 1.1 理解网络编程和套接字 6 | 7 | #### 1.1.1构建打电话套接字 8 | 9 | 以电话机打电话的方式来理解套接字。 10 | 11 | **调用 socket 函数(安装电话机)时进行的对话**: 12 | 13 | > 问:接电话需要准备什么? 14 | > 15 | > 答:当然是电话机。 16 | 17 | 有了电话机才能安装电话,于是就要准备一个电话机,下面函数相当于电话机的套接字。 18 | 19 | ```c 20 | #include 21 | int socket(int domain, int type, int protocol); 22 | //成功时返回文件描述符,失败时返回-1 23 | ``` 24 | 25 | **调用 bind 函数(分配电话号码)时进行的对话**: 26 | 27 | > 问:请问我的电话号码是多少 28 | > 29 | > 答:我的电话号码是123-1234 30 | 31 | 套接字同样如此。就想给电话机分配电话号码一样,利用以下函数给创建好的套接字分配地址信息(IP地址和端口号): 32 | 33 | ```c 34 | #include 35 | int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); 36 | //成功时返回0,失败时返回-1 37 | ``` 38 | 39 | 调用 bind 函数给套接字分配地址之后,就基本完成了所有的准备工作。接下来是需要连接电话线并等待来电。 40 | 41 | **调用 listen 函数(连接电话线)时进行的对话**: 42 | 43 | > 问:已架设完电话机后是否只需链接电话线? 44 | > 45 | > 答:对,只需要连接就能接听电话。 46 | 47 | 一连接电话线,电话机就可以转换为可接听状态,这时其他人可以拨打电话请求连接到该机。同样,需要把套接字转化成可接受连接状态。 48 | 49 | ```c 50 | #include 51 | int listen(int sockfd, int backlog); 52 | //成功时返回0,失败时返回-1 53 | ``` 54 | 55 | 连接好电话线以后,如果有人拨打电话就响铃,拿起话筒才能接听电话。 56 | 57 | **调用 accept 函数(拿起话筒)时进行的对话**: 58 | 59 | > 问:电话铃响了,我该怎么办? 60 | > 61 | > 答:接听啊。 62 | 63 | ```c 64 | #include 65 | int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); 66 | //成功时返回文件描述符,失败时返回-1 67 | ``` 68 | 69 | 网络编程中和接受连接请求的套接字创建过程可整理如下: 70 | 71 | 1. 第一步:调用 socket 函数创建套接字。 72 | 2. 第二步:调用 bind 函数分配IP地址和端口号。 73 | 3. 第三步:调用 listen 函数转换为可接受请求状态。 74 | 4. 第四步:调用 accept 函数受理套接字请求。 75 | 76 | #### 1.1.2 编写`Hello World`套接字程序 77 | 78 | **服务端**: 79 | 80 | 服务器端(server)是能够受理连接请求的程序。下面构建服务端以验证之前提到的函数调用过程,该服务器端收到连接请求后向请求者返回`Hello World!`答复。除各种函数的调用顺序外,我们还未涉及任何实际编程。因此,阅读代码时请重点关注套接字相关的函数调用过程,不必理解全过程。 81 | 82 | 服务器端代码请参见:[hello_server.c](ch01/hello_server.c) 83 | 84 | **客户端**: 85 | 86 | 客户端程序只有`调用 socket 函数创建套接字` 和 `调用 connect 函数向服务端发送连接请求`这两个步骤,下面给出客户端,需要查看以下两方面的内容: 87 | 88 | 1. 调用 socket 函数 和 connect 函数 89 | 2. 与服务端共同运行以收发字符串数据 90 | 91 | 客户端代码请参见:[hello_client.c](ch01/hello_client.c) 92 | 93 | **编译**: 94 | 95 | 分别对客户端和服务端程序进行编译: 96 | 97 | ```shell 98 | gcc hello_server.c -o hserver 99 | gcc hello_client.c -o hclient 100 | ``` 101 | 102 | **运行**: 103 | 104 | ```shell 105 | ./hserver 9190 106 | ./hclient 127.0.0.1 9190 107 | ``` 108 | 109 | 运行的时候,首先再 9190 端口启动服务,然后 heserver 就会一直等待客户端进行响应,当客户端监听位于本地的 IP 为 127.0.0.1 的地址的9190端口时,客户端就会收到服务端的回应,输出`Hello World!` 110 | 111 | ### 1.2 基于 Linux 的文件操作 112 | 113 | 讨论套接字的过程中突然谈及文件也许有些奇怪。但是对于 Linux 而言,socket 操作与文件操作没有区别,因而有必要详细了解文件。在 Linux 世界里,socket 也被认为是文件的一种,因此在网络数据传输过程中自然可以使用 I/O 的相关函数。Windows 与 Linux 不同,是要区分 socket 和文件的。因此在 Windows 中需要调用特殊的数据传输相关函数。 114 | 115 | #### 1.2.1 底层访问和文件描述符 116 | 117 | 分配给标准输入输出及标准错误的文件描述符。 118 | 119 | | 文件描述符 | 对象 | 120 | | :--------: | :-----------------------: | 121 | | 0 | 标准输入:Standard Input | 122 | | 1 | 标准输出:Standard Output | 123 | | 2 | 标准错误:Standard Error | 124 | 125 | 文件和套接字一般经过创建过程才会被分配文件描述符。 126 | 127 | 文件描述符也被称为「文件句柄」,但是「句柄」主要是 Windows 中的术语。因此,在本书中如果设计 Windows 平台将使用「句柄」,如果是 Linux 将使用「描述符」。 128 | 129 | #### 1.2.2 打开文件: 130 | 131 | ```c 132 | #include 133 | #include 134 | #include 135 | int open(const char *path, int flag); 136 | /* 137 | 成功时返回文件描述符,失败时返回-1 138 | path : 文件名的字符串地址 139 | flag : 文件打开模式信息 140 | */ 141 | ``` 142 | 143 | 文件打开模式如下表: 144 | 145 | | 打开模式 | 含义 | 146 | | :------: | :------------------------: | 147 | | O_CREAT | 必要时创建文件 | 148 | | O_TRUNC | 删除全部现有数据 | 149 | | O_APPEND | 维持现有数据,保存到其后面 | 150 | | O_RDONLY | 只读打开 | 151 | | O_WRONLY | 只写打开 | 152 | | O_RDWR | 读写打开 | 153 | 154 | #### 1.2.3 关闭文件: 155 | 156 | ```c 157 | #include 158 | int close(int fd); 159 | /* 160 | 成功时返回 0 ,失败时返回 -1 161 | fd : 需要关闭的文件或套接字的文件描述符 162 | */ 163 | ``` 164 | 165 | 若调用此函数同时传递文件描述符参数,则关闭(终止)响应文件。另外需要注意的是,此函数不仅可以关闭文件,还可以关闭套接字。再次证明了「Linux 操作系统不区分文件与套接字」的特点。 166 | 167 | #### 1.2.4 将数据写入文件: 168 | 169 | ```c 170 | #include 171 | ssize_t write(int fd, const void *buf, size_t nbytes); 172 | /* 173 | 成功时返回写入的字节数 ,失败时返回 -1 174 | fd : 显示数据传输对象的文件描述符 175 | buf : 保存要传输数据的缓冲值地址 176 | nbytes : 要传输数据的字节数 177 | */ 178 | ``` 179 | 180 | 在此函数的定义中,size_t 是通过 typedef 声明的 unsigned int 类型。对 ssize_t 来说,ssize_t 前面多加的 s 代表 signed ,即 ssize_t 是通过 typedef 声明的 signed int 类型。 181 | 182 | 创建新文件并保存数据: 183 | 184 | 代码见:[low_open.c](ch01/low_open.c) 185 | 186 | 编译运行: 187 | 188 | ```shell 189 | gcc low_open.c -o lopen 190 | ./lopen 191 | ``` 192 | 193 | 然后会生成一个`data.txt`的文件,里面有`Let's go!` 194 | 195 | #### 1.2.5 读取文件中的数据: 196 | 197 | 与之前的`write()`函数相对应,`read()`用来输入(接收)数据。 198 | 199 | ```c 200 | #include 201 | ssize_t read(int fd, void *buf, size_t nbytes); 202 | /* 203 | 成功时返回接收的字节数(但遇到文件结尾则返回 0),失败时返回 -1 204 | fd : 显示数据接收对象的文件描述符 205 | buf : 要保存接收的数据的缓冲地址值。 206 | nbytes : 要接收数据的最大字节数 207 | */ 208 | ``` 209 | 210 | 下面示例通过 read() 函数读取 data.txt 中保存的数据。 211 | 212 | 代码见:[low_read.c](ch01/low_read.c) 213 | 214 | 编译运行: 215 | 216 | ```shell 217 | gcc low_read.c -o lread 218 | ./lread 219 | ``` 220 | 221 | 在上一步的 data.txt 文件与没有删的情况下,会输出: 222 | 223 | ``` 224 | file descriptor: 3 225 | file data: Let's go! 226 | ``` 227 | 228 | 关于文件描述符的 I/O 操作到此结束,要明白,这些内容同样适合于套接字。 229 | 230 | #### 1.2.6 文件描述符与套接字 231 | 232 | 下面将同时创建文件和套接字,并用整数型态比较返回的文件描述符的值. 233 | 234 | 代码见:[fd_seri.c](ch01/fd_seri.c) 235 | 236 | **编译运行**: 237 | 238 | ```shell 239 | gcc fd_seri.c -o fds 240 | ./fds 241 | ``` 242 | 243 | **输出结果**: 244 | 245 | ``` 246 | file descriptor 1: 3 247 | file descriptor 2: 15 248 | file descriptor 3: 16 249 | ``` 250 | 251 | ### 1.3 基于 Windows 平台的实现 252 | 253 | 暂略 254 | 255 | ### 1.4 基于 Windows 的套接字相关函数及示例 256 | 257 | 暂略 258 | 259 | ### 1.5 习题 260 | 261 | > :heavy_exclamation_mark:以下部分的答案,仅代表我个人观点,可能不是正确答案 262 | 263 | 1. 套接字在网络编程中的作用是什么?为何称它为套接字? 264 | 265 | > 答:操作系统会提供「套接字」(socket)的部件,套接字是网络数据传输用的软件设备。因此,「网络编程」也叫「套接字编程」。「套接字」就是用来连接网络的工具。 266 | 267 | 2. 在服务器端创建套接字以后,会依次调用 listen 函数和 accept 函数。请比较二者作用。 268 | 269 | > 答:调用 listen 函数将套接字转换成可受连接状态(监听),调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。 270 | 271 | 3. Linux 中,对套接字数据进行 I/O 时可以直接使用文件 I/O 相关函数;而在 Windows 中则不可以。原因为何? 272 | 273 | > 答:暂略。 274 | 275 | 4. 创建套接字后一般会给他分配地址,为什么?为了完成地址分配需要调用哪个函数? 276 | 277 | > 答:套接字被创建之后,只有为其分配了IP地址和端口号后,客户端才能够通过IP地址及端口号与服务器端建立连接,需要调用 bind 函数来完成地址分配。 278 | 279 | 5. Linux 中的文件描述符与 Windows 的句柄实际上非常类似。请以套接字为对象说明它们的含义。 280 | 281 | > 答:暂略。 282 | 283 | 6. 底层 I/O 函数与 ANSI 标准定义的文件 I/O 函数有何区别? 284 | 285 | > 答:文件 I/O 又称为低级磁盘 I/O,遵循 POSIX 相关标准。任何兼容 POSIX 标准的操作系统上都支持文件I/O。标准 I/O 被称为高级磁盘 I/O,遵循 ANSI C 相关标准。只要开发环境中有标准 I/O 库,标准 I/O 就可以使用。(Linux 中使用的是 GLIBC,它是标准C库的超集。不仅包含 ANSI C 中定义的函数,还包括 POSIX 标准中定义的函数。因此,Linux 下既可以使用标准 I/O,也可以使用文件 I/O)。 286 | 287 | 7. 参考本书给出的示例`low_open.c`和`low_read.c`,分别利用底层文件 I/O 和 ANSI 标准 I/O 编写文件复制程序。可任意指定复制程序的使用方法。 288 | 289 | > 答:暂略。 290 | -------------------------------------------------------------------------------- /ch01/fd_seri.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main() 7 | { 8 | int fd1, fd2, fd3; 9 | //创建一个文件和两个套接字 10 | fd1 = socket(PF_INET, SOCK_STREAM, 0); 11 | fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC); 12 | fd3 = socket(PF_INET, SOCK_DGRAM, 0); 13 | //输出之前创建的文件描述符的整数值 14 | printf("file descriptor 1: %d\n", fd1); 15 | printf("file descriptor 2: %d\n", fd2); 16 | printf("file descriptor 3: %d\n", fd3); 17 | 18 | close(fd1); 19 | close(fd2); 20 | close(fd3); 21 | return 0; 22 | } -------------------------------------------------------------------------------- /ch01/hello_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int sock; 12 | struct sockaddr_in serv_addr; 13 | char message[30]; 14 | int str_len; 15 | 16 | if (argc != 3) 17 | { 18 | printf("Usage : %s \n", argv[0]); 19 | exit(1); 20 | } 21 | //创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字 22 | //如果调用 connect 函数,将成为客户端套接字 23 | sock = socket(PF_INET, SOCK_STREAM, 0); 24 | if (sock == -1) 25 | error_handling("socket() error"); 26 | 27 | memset(&serv_addr, 0, sizeof(serv_addr)); 28 | serv_addr.sin_family = AF_INET; 29 | serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 30 | serv_addr.sin_port = htons(atoi(argv[2])); 31 | //调用 connect 函数向服务器发送连接请求 32 | if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 33 | error_handling("connect() error!"); 34 | 35 | str_len = read(sock, message, sizeof(message) - 1); 36 | if (str_len == -1) 37 | error_handling("read() error!"); 38 | 39 | printf("Message from server : %s \n", message); 40 | close(sock); 41 | return 0; 42 | } 43 | 44 | void error_handling(char *message) 45 | { 46 | fputs(message, stderr); 47 | fputc('\n', stderr); 48 | exit(1); 49 | } -------------------------------------------------------------------------------- /ch01/hello_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int serv_sock; 12 | int clnt_sock; 13 | 14 | struct sockaddr_in serv_addr; 15 | struct sockaddr_in clnt_addr; 16 | socklen_t clnt_addr_size; 17 | 18 | char message[] = "Hello World!"; 19 | 20 | if (argc != 2) 21 | { 22 | printf("Usage : %s \n", argv[0]); 23 | exit(1); 24 | } 25 | //调用 socket 函数创建套接字 26 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 27 | if (serv_sock == -1) 28 | error_handling("socket() error"); 29 | 30 | memset(&serv_addr, 0, sizeof(serv_addr)); 31 | serv_addr.sin_family = AF_INET; 32 | serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 33 | serv_addr.sin_port = htons(atoi(argv[1])); 34 | //调用 bind 函数分配ip地址和端口号 35 | if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 36 | error_handling("bind() error"); 37 | //调用 listen 函数将套接字转为可接受连接状态 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | clnt_addr_size = sizeof(clnt_addr); 42 | //调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 43 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); 44 | if (clnt_sock == -1) 45 | error_handling("accept() error"); 46 | //稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求 47 | write(clnt_sock, message, sizeof(message)); 48 | close(clnt_sock); 49 | close(serv_sock); 50 | return 0; 51 | } 52 | 53 | void error_handling(char *message) 54 | { 55 | fputs(message, stderr); 56 | fputc('\n', stderr); 57 | exit(1); 58 | } -------------------------------------------------------------------------------- /ch01/low_open.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | void error_handling(char *message); 6 | 7 | int main() 8 | { 9 | int fd; 10 | char buf[] = "Let's go!\n"; 11 | // O_CREAT | O_WRONLY | O_TRUNC 是文件打开模式,将创建新文件,并且只能写。如存在 data.txt 文件,则清空文件中的全部数据。 12 | fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC); 13 | if (fd == -1) 14 | error_handling("open() error!"); 15 | printf("file descriptor: %d \n", fd); 16 | // 向对应 fd 中保存的文件描述符的文件传输 buf 中保存的数据。 17 | if (write(fd, buf, sizeof(buf)) == -1) 18 | error_handling("write() error!"); 19 | close(fd); 20 | return 0; 21 | } 22 | 23 | void error_handling(char *message) 24 | { 25 | fputs(message, stderr); 26 | fputc('\n', stderr); 27 | exit(1); 28 | } -------------------------------------------------------------------------------- /ch01/low_read.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #define BUF_SIZE 100 6 | void error_handling(char *message); 7 | 8 | int main() 9 | { 10 | int fd; 11 | char buf[BUF_SIZE]; 12 | 13 | fd = open("data.txt", O_RDONLY); 14 | if (fd == -1) 15 | error_handling("open() error!"); 16 | printf("file descriptor: %d \n", fd); 17 | 18 | if (read(fd, buf, sizeof(buf)) == -1) 19 | error_handling("read() error!"); 20 | printf("file data: %s", buf); 21 | close(fd); 22 | return 0; 23 | } 24 | void error_handling(char *message) 25 | { 26 | fputs(message, stderr); 27 | fputc('\n', stderr); 28 | exit(1); 29 | } -------------------------------------------------------------------------------- /ch02/README.md: -------------------------------------------------------------------------------- 1 | ## 第二章 套接字类型与协议设置 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点连接可能进不去。 4 | 5 | 本章仅需了解创建套接字时调用的 socket 函数。 6 | 7 | ### 2.1 套接字协议及数据传输特性 8 | 9 | #### 2.1.1 创建套接字 10 | 11 | ```c 12 | #include 13 | int socket(int domain, int type, int protocol); 14 | /* 15 | 成功时返回文件描述符,失败时返回-1 16 | domain: 套接字中使用的协议族(Protocol Family) 17 | type: 套接字数据传输的类型信息 18 | protocol: 计算机间通信中使用的协议信息 19 | */ 20 | ``` 21 | 22 | #### 2.1.2 协议族(Protocol Family) 23 | 24 | 通过 socket 函数的第一个参数传递套接字中使用的协议分类信息。此协议分类信息称为协议族,可分成如下几类: 25 | 26 | > 头文件 `sys/socket.h` 中声明的协议族 27 | > 28 | 29 | | 名称 | 协议族 | 30 | | --------- | -------------------- | 31 | | PF_INET | IPV4 互联网协议族 | 32 | | PF_INET6 | IPV6 互联网协议族 | 33 | | PF_LOCAL | 本地通信 Unix 协议族 | 34 | | PF_PACKET | 底层套接字的协议族 | 35 | | PF_IPX | IPX Novel 协议族 | 36 | 37 | 本书着重讲 PF_INET 对应的 IPV4 互联网协议族。其他协议并不常用,或并未普及。**另外,套接字中采用的最终的协议信息是通过 socket 函数的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数。** 38 | 39 | #### 2.1.3 套接字类型(Type) 40 | 41 | 套接字类型指的是套接字的数据传输方式,是通过 socket 函数的第二个参数进行传递,只有这样才能决定创建的套接字的数据传输方式。**已经通过第一个参数传递了协议族信息,为什么还要决定数据传输方式?问题就在于,决定了协议族并不能同时决定数据传输方式。换言之, socket 函数的第一个参数 PF_INET 协议族中也存在多种数据传输方式。** 42 | 43 | #### 2.1.4 套接字类型1:面向连接的套接字(SOCK_STREAM) 44 | 45 | 如果 socket 函数的第二个参数传递`SOCK_STREAM`,将创建面向连接的套接字。 46 | 47 | 传输方式特征整理如下: 48 | 49 | - 传输过程中数据不会消失 50 | - 按序传输数据 51 | - 传输的数据不存在数据边界(Boundary) 52 | 53 | 这种情形适用于之前说过的 write 和 read 函数 54 | 55 | > 传输数据的计算机通过调用3次 write 函数传递了 100 字节的数据,但是接受数据的计算机仅仅通过调用 1 次 read 函数调用就接受了全部 100 个字节。 56 | 57 | 收发数据的套接字内部有缓冲(buffer),简言之就是字节数组。只要不超过数组容量,那么数据填满缓冲后过 1 次 read 函数的调用就可以读取全部,也有可能调用多次来完成读取。 58 | 59 | **套接字缓冲已满是否意味着数据丢失?** 60 | 61 | > 答:缓冲并不总是满的。如果读取速度比数据传入过来的速度慢,则缓冲可能被填满,但是这时也不会丢失数据,因为传输套接字此时会停止数据传输,所以面向连接的套接字不会发生数据丢失。 62 | 63 | 套接字联机必须一一对应。面向连接的套接字可总结为: 64 | 65 | **可靠地、按序传递的、基于字节的面向连接的数据传输方式的套接字。** 66 | 67 | #### 2.1.5 面向消息的套接字(SOCK_DGRAM) 68 | 69 | 如果 socket 函数的第二个参数传递`SOCK_DGRAM`,则将创建面向消息的套接字。面向消息的套接字可以比喻成高速移动的摩托车队。特点如下: 70 | 71 | - 强调快速传输而非传输有序 72 | - 传输的数据可能丢失也可能损毁 73 | - 传输的数据有边界 74 | - 限制每次传输数据的大小 75 | 76 | 面向消息的套接字比面向连接的套接字更具有传输速度,但可能丢失。特点可总结为: 77 | 78 | **不可靠的、不按序传递的、以数据的高速传输为目的套接字。** 79 | 80 | #### 2.1.6 协议的最终选择 81 | 82 | socket 函数的第三个参数决定最终采用的协议。前面已经通过前两个参数传递了协议族信息和套接字数据传输方式,这些信息还不够吗?为什么要传输第三个参数呢? 83 | 84 | > 可以应对同一协议族中存在的多个数据传输方式相同的协议,所以数据传输方式相同,但是协议不同,需要用第三个参数指定具体的协议信息。 85 | 86 | 本书用的是 Ipv4 的协议族,和面向连接的数据传输,满足这两个条件的协议只有 IPPROTO_TCP ,因此可以如下调用 socket 函数创建套接字,这种套接字称为 TCP 套接字。 87 | 88 | ```c 89 | int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); 90 | ``` 91 | 92 | SOCK_DGRAM 指的是面向消息的数据传输方式,满足上述条件的协议只有 IPPROTO_UDP 。这种套接字称为 UDP 套接字: 93 | 94 | ```c 95 | int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); 96 | ``` 97 | 98 | #### 2.1.7 面向连接的套接字:TCP 套接字示例 99 | 100 | 需要对第一章的代码做出修改,修改好的代码如下: 101 | 102 | - [tcp_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch02/tcp_client.c) 103 | - [tcp_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch02/tcp_server.c) 104 | 105 | 编译: 106 | 107 | ```shell 108 | gcc tcp_client.c -o hclient 109 | gcc tcp_server.c -o hserver 110 | ``` 111 | 112 | 运行: 113 | 114 | ```shell 115 | ./hserver 9190 116 | ./hclient 127.0.0.1 9190 117 | ``` 118 | 119 | 结果: 120 | 121 | ``` 122 | Message from server : Hello World! 123 | Function read call count: 13 124 | ``` 125 | 126 | 从运行结果可以看出服务端发送了13字节的数据,客户端调用13次 read 函数进行读取。 127 | 128 | ### 2.2 Windows 平台下的实现及验证 129 | 130 | 暂略 131 | 132 | ### 2.3 习题 133 | 134 | 1. 什么是协议?在收发数据中定义协议有何意义? 135 | 136 | > 答:协议是对话中使用的通信规则,简言之,协议就是为了完成数据交换而定好的约定。在收发数据中定义协议,能够让计算机之间进行对话,以此来实现信息交换和资源共享。 137 | 138 | 2. 面向连接的套接字 TCP 套接字传输特性有 3 点,请分别说明。 139 | 140 | > 答:①传输过程中数据不会消失②按序传输数据③传输的数据不存在数据边界(Boundary) 141 | 142 | 3. 下面那些是面向消息的套接字的特性? 143 | 144 | - **传输数据可能丢失** 145 | - 没有数据边界(Boundary) 146 | - **以快速传递为目标** 147 | - 不限制每次传输数据大小 148 | - **与面向连接的套接字不同,不存在连接概念** 149 | 150 | 4. 下列数据适合用哪类套接字进行传输? 151 | 152 | - 演唱会现场直播的多媒体数据(UDP) 153 | - 某人压缩过的文本文件(TCP) 154 | - 网上银行用户与银行之间的数据传递(TCP) 155 | 156 | 5. 何种类型的套接字不存在数据边界?这类套接字接收数据时应该注意什么? 157 | 158 | > 答:TCP 不存在数据边界。在接收数据时,需要保证在接收套接字的缓冲区填充满之时就从buffer里读取数据。也就是,在接收套接字内部,写入buffer的速度要小于读出buffer的速度。 159 | -------------------------------------------------------------------------------- /ch02/tcp_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int sock; 12 | struct sockaddr_in serv_addr; 13 | char message[30]; 14 | int str_len = 0; 15 | int idx = 0, read_len = 0; 16 | 17 | if (argc != 3) 18 | { 19 | printf("Usage : %s \n", argv[0]); 20 | exit(1); 21 | } 22 | //创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字 23 | //如果调用 connect 函数,将成为客户端套接字 24 | //若前两个参数使用PF_INET 和 SOCK_STREAM,则可以省略第三个参数 IPPROTO_TCP 25 | sock = socket(PF_INET, SOCK_STREAM, 0); 26 | if (sock == -1) 27 | error_handling("socket() error"); 28 | 29 | memset(&serv_addr, 0, sizeof(serv_addr)); 30 | serv_addr.sin_family = AF_INET; 31 | serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 32 | serv_addr.sin_port = htons(atoi(argv[2])); 33 | //调用 connect 函数向服务器发送连接请求 34 | if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 35 | error_handling("connect() error!"); 36 | //当read函数返回0的时候条件为假,跳出循环。 37 | while (read_len = read(sock, &message[idx++], 1)) 38 | { 39 | if (read_len == -1) 40 | error_handling("read() error!"); 41 | str_len += read_len; 42 | } 43 | printf("Message from server : %s \n", message); 44 | printf("Function read call count: %d \n", str_len); 45 | close(sock); 46 | return 0; 47 | } 48 | 49 | void error_handling(char *message) 50 | { 51 | fputs(message, stderr); 52 | fputc('\n', stderr); 53 | exit(1); 54 | } -------------------------------------------------------------------------------- /ch02/tcp_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int serv_sock; 12 | int clnt_sock; 13 | 14 | struct sockaddr_in serv_addr; 15 | struct sockaddr_in clnt_addr; 16 | socklen_t clnt_addr_size; 17 | 18 | char message[] = "Hello World!"; 19 | 20 | if (argc != 2) 21 | { 22 | printf("Usage : %s \n", argv[0]); 23 | exit(1); 24 | } 25 | //调用 socket 函数创建套接字 26 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 27 | if (serv_sock == -1) 28 | error_handling("socket() error"); 29 | 30 | memset(&serv_addr, 0, sizeof(serv_addr)); 31 | serv_addr.sin_family = AF_INET; 32 | serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 33 | serv_addr.sin_port = htons(atoi(argv[1])); 34 | //调用 bind 函数分配ip地址和端口号 35 | if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 36 | error_handling("bind() error"); 37 | //调用 listen 函数将套接字转为可接受连接状态 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | clnt_addr_size = sizeof(clnt_addr); 42 | //调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 43 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); 44 | if (clnt_sock == -1) 45 | error_handling("accept() error"); 46 | //稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求 47 | write(clnt_sock, message, sizeof(message)); 48 | close(clnt_sock); 49 | close(serv_sock); 50 | return 0; 51 | } 52 | 53 | void error_handling(char *message) 54 | { 55 | fputs(message, stderr); 56 | fputc('\n', stderr); 57 | exit(1); 58 | } -------------------------------------------------------------------------------- /ch03/endian_conv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | int main(int argc, char *argv[]) 4 | { 5 | unsigned short host_port = 0x1234; 6 | unsigned short net_port; 7 | unsigned long host_addr = 0x12345678; 8 | unsigned long net_addr; 9 | 10 | net_port = htons(host_port); //转换为网络字节序 11 | net_addr = htonl(host_addr); 12 | 13 | printf("Host ordered port: %#x \n", host_port); 14 | printf("Network ordered port: %#x \n", net_port); 15 | printf("Host ordered address: %#lx \n", host_addr); 16 | printf("Network ordered address: %#lx \n", net_addr); 17 | 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /ch03/inet_addr.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | int main(int argc, char *argv[]) 4 | { 5 | char *addr1 = "1.2.3.4"; 6 | char *addr2 = "1.2.3.256"; 7 | 8 | unsigned long conv_addr = inet_addr(addr1); 9 | if (conv_addr == INADDR_NONE) 10 | printf("Error occured! \n"); 11 | else 12 | printf("Network ordered integer addr: %#lx \n", conv_addr); 13 | 14 | conv_addr = inet_addr(addr2); 15 | if (conv_addr == INADDR_NONE) 16 | printf("Error occured! \n"); 17 | else 18 | printf("Network ordered integer addr: %#lx \n", conv_addr); 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /ch03/inet_aton.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | void error_handling(char *message); 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | char *addr = "127.232.124.79"; 9 | struct sockaddr_in addr_inet; 10 | 11 | if (!inet_aton(addr, &addr_inet.sin_addr)) 12 | error_handling("Conversion error"); 13 | else 14 | printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr); 15 | return 0; 16 | } 17 | 18 | void error_handling(char *message) 19 | { 20 | fputs(message, stderr); 21 | fputc('\n', stderr); 22 | exit(1); 23 | } -------------------------------------------------------------------------------- /ch03/inet_ntoa.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | struct sockaddr_in addr1, addr2; 8 | char *str_ptr; 9 | char str_arr[20]; 10 | 11 | addr1.sin_addr.s_addr = htonl(0x1020304); 12 | addr2.sin_addr.s_addr = htonl(0x1010101); 13 | //把addr1中的结构体信息转换为字符串的IP地址形式 14 | str_ptr = inet_ntoa(addr1.sin_addr); 15 | strcpy(str_arr, str_ptr); 16 | printf("Dotted-Decimal notation1: %s \n", str_ptr); 17 | 18 | inet_ntoa(addr2.sin_addr); 19 | printf("Dotted-Decimal notation2: %s \n", str_ptr); 20 | printf("Dotted-Decimal notation3: %s \n", str_arr); 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /ch04/README.md: -------------------------------------------------------------------------------- 1 | ## 第 4 章 基于 TCP 的服务端/客户端(1) 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | ### 4.1 理解 TCP 和 UDP 6 | 7 | 根据数据传输方式的不同,基于网络协议的套接字一般分为 TCP 套接字和 UDP 套接字。因为 TCP 套接字是面向连接的,因此又被称为基于流(stream)的套接字。 8 | 9 | TCP 是 Transmission Control Protocol (传输控制协议)的简写,意为「对数据传输过程的控制」。因此,学习控制方法及范围有助于正确理解 TCP 套接字。 10 | 11 | #### 4.1.1 TCP/IP 协议栈 12 | 13 | ![](https://i.loli.net/2019/01/14/5c3c21889db06.png) 14 | 15 | TCP/IP 协议栈共分为 4 层,可以理解为数据收发分成了 4 个层次化过程,通过层次化的方式来解决问题 16 | 17 | #### 4.1.2 链路层 18 | 19 | 链路层是物理链接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN等网络标准。若两台主机通过网络进行数据交换,则需要物理连接,链路层就负责这些标准。 20 | 21 | #### 4.1.3 IP 层 22 | 23 | 准备好物理连接后就要传输数据。为了在复杂网络中传输数据,首先要考虑路径的选择。向目标传输数据需要经过哪条路径?解决此问题的就是IP层,该层使用的协议就是IP。 24 | 25 | IP 是面向消息的、不可靠的协议。每次传输数据时会帮我们选择路径,但并不一致。如果传输过程中发生错误,则选择其他路径,但是如果发生数据丢失或错误,则无法解决。换言之,IP协议无法应对数据错误。 26 | 27 | #### 4.1.4 TCP/UDP 层 28 | 29 | IP 层解决数据传输中的路径选择问题,只需照此路径传输数据即可。TCP 和 UDP 层以 IP 层提供的路径信息为基础完成实际的数据传输,故该层又称为传输层。UDP 比 TCP 简单,现在我们只解释 TCP 。 TCP 可以保证数据的可靠传输,但是它发送数据时以 IP 层为基础(这也是协议栈层次化的原因)。 30 | 31 | IP 层只关注一个数据包(数据传输基本单位)的传输过程。因此,即使传输多个数据包,每个数据包也是由 IP 层实际传输的,也就是说传输顺序及传输本身是不可靠的。若只利用IP层传输数据,则可能导致后传输的数据包B比先传输的数据包A提早到达。另外,传输的数据包A、B、C中可能只收到A和C,甚至收到的C可能已经损毁 。反之,若添加 TCP 协议则按照如下对话方式进行数据交换。 32 | 33 | > 主机A:正确接受第二个数据包 34 | > 35 | > 主机B:恩,知道了 36 | > 37 | > 主机A:正确收到第三个数据包 38 | > 39 | > 主机B:可我已经发送第四个数据包了啊!哦,您没收到吧,我给你重新发。 40 | 41 | 这就是 TCP 的作用。如果交换数据的过程中可以确认对方已经收到数据,并重传丢失的数据,那么即便IP层不保证数据传输,这类通信也是可靠的。 42 | 43 | ![](https://i.loli.net/2019/01/14/5c3c268b40be6.png) 44 | 45 | #### 4.1.5 应用层 46 | 47 | 上述内容是套接字通信过程中自动处理的。选择数据传输路径、数据确认过程都被隐藏到套接字内部。向程序员提供的工具就是套接字,只需要利用套接字编出程序即可。编写软件的过程中,需要根据程序的特点来决定服务器和客户端之间的数据传输规则,这便是应用层协议。 48 | 49 | ### 4.2 实现基于 TCP 的服务器/客户端 50 | 51 | #### 4.2.1 TCP 服务端的默认函数的调用程序 52 | 53 | ![](https://i.loli.net/2019/01/14/5c3c2782a7810.png) 54 | 55 | 调用 socket 函数创建套接字,声明并初始化地址信息的结构体变量,调用 bind 函数向套接字分配地址。 56 | 57 | #### 4.2.2 进入等待连接请求状态 58 | 59 | 已经调用了 bind 函数给套接字分配地址,接下来就是要通过调用 listen 函数进入等待链接请求状态。只有调用了 listen 函数,客户端才能进入可发出连接请求的状态。客户端可以调用 connect 函数,向服务端请求连接,对于客户端发来的请求,先进入连接请求等待队列,等待服务端受理请求。 60 | 61 | ```c 62 | #include 63 | int listen(int sockfd, int backlog); 64 | //成功时返回0,失败时返回-1 65 | //sock: 希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数称为服务端套接字 66 | //backlog: 连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列 67 | ``` 68 | #### 4.2.3 受理客户端连接请求 69 | 70 | 调用 listen 函数后,套接字应该按序受理客户端发起的连接请求。受理请求就是服务端处理一个连接请求,进入可接受客户端数据的状态。进入这种状态所需的部件是**套接字**,但是此时使用的不是服务端套接字,此时需要另一个套接字,但是没必要亲自创建,下面的函数将自动创建套接字。 71 | 72 | ```c 73 | #include 74 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 75 | /* 76 | 成功时返回文件描述符,失败时返回-1 77 | sock: 服务端套接字的文件描述符 78 | addr: 受理的请求中,客户端地址信息会保存到该指针指向的地址 79 | addrlen: 该指针指向的地址中保存第二个参数的结构体长度 80 | */ 81 | ``` 82 | 83 | accept 函数受理连接请求队列中待处理的客户端连接请求。函数调用成功后,accept 内部将产生用于数据 I/O 的套接字,并返回其文件描述符。需要强调的是套接字是自动创建的,并自动与发起连接请求的客户端建立连接。 84 | 85 | 注意:accept 函数返回的套接字不等于服务端套接字,也需要通过 close 函数关闭。 86 | 87 | #### 4.2.4 回顾 Hello World 服务端 88 | 89 | - 代码:[hello_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/hello_server.c) 90 | 91 | 重新整理一下代码的思路 92 | 93 | 1. 服务端实现过程中首先要创建套接字,此时的套接字并非是真正的服务端套接字 94 | 2. 为了完成套接字地址的分配,初始化结构体变量并调用 bind 函数。 95 | 3. 调用 listen 函数进入等待连接请求状态。连接请求状态队列的长度设置为5.此时的套接字才是服务端套接字。 96 | 4. 调用 accept 函数从队头取 1 个连接请求与客户端建立连接,并返回创建的套接字文件描述符。另外,调用 accept 函数时若等待队列为空,则 accept 函数不会返回,直到队列中出现新的客户端连接。 97 | 5. 调用 write 函数向客户端传送数据,调用 close 关闭连接 98 | 99 | #### 4.2.5 TCP 客户端的默认函数调用顺序 100 | 101 | ![](https://i.loli.net/2019/01/14/5c3c31d77e86c.png) 102 | 103 | 与服务端相比,区别就在于「请求连接」,它是创建客户端套接字后向服务端发起的连接请求。服务端调用 listen 函数后创建连接请求等待队列,之后客户端即可请求连接。 104 | 105 | ```c 106 | #include 107 | int connect(int sock, struct sockaddr *servaddr, socklen_t addrlen); 108 | /* 109 | 成功时返回0,失败返回-1 110 | sock:客户端套接字文件描述符 111 | servaddr: 保存目标服务器端地址信息的变量地址值 112 | addrlen: 第二个结构体参数 servaddr 变量的字节长度 113 | */ 114 | ``` 115 | 116 | 客户端调用 connect 函数后,发生以下函数之一才会返回(完成函数调用): 117 | 118 | - 服务端接受连接请求 119 | - 发生断网等异常状况而中断连接请求 120 | 121 | 注意:**接受连接**不代表服务端调用 accept 函数,其实只是服务器端把连接请求信息记录到等待队列。因此 connect 函数返回后并不应该立即进行数据交换。 122 | 123 | 客户端在调用connect函数时自动分配主机的IP,随机分配端口。无需调用标记的bind函数进行分配。 124 | 125 | #### 4.2.6 回顾 Hello World 客户端 126 | 127 | - 代码:[hello_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/hello_client.c) 128 | 129 | 重新理解这个程序: 130 | 131 | 1. 创建准备连接服务器的套接字,此时创建的是 TCP 套接字 132 | 2. 结构体变量 serv_addr 中初始化IP和端口信息。初始化值为目标服务器端套接字的IP和端口信息。 133 | 3. 调用 connect 函数向服务端发起连接请求 134 | 4. 完成连接后,接收服务端传输的数据 135 | 5. 接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。(对套接字调用close函数,对应于向建立连接的对应套接字发送EOF。即,如果客户端的套接字调用了close函数,服务端read时候会返回0。) 136 | 137 | #### 4.2.7 基于 TCP 的服务端/客户端函数调用关系 138 | 139 | 关系图如下所示: 140 | 141 | ![](https://i.loli.net/2019/01/14/5c3c35a773b8c.png) 142 | 143 | - 客户端只能等到服务端调用 listen 函数后才才能调用 connect 函数 144 | - 服务器端可能会在客户端调用 connect 之前调用 accept 函数,这时服务器端进入阻塞(blocking)状态,直到客户端调用 connect 函数后接收到连接请求。 145 | 146 | ### 4.3 实现迭代服务端/客户端 147 | 148 | 编写一个回声(echo)服务器/客户端。顾名思义,服务端将客户端传输的字符串数据原封不动的传回客户端,就像回声一样。在此之前,需要解释一下迭代服务器端。 149 | 150 | #### 4.3.1 实现迭代服务器端 151 | 152 | 在 Hello World 的例子中,等待队列的作用没有太大意义。如果想继续处理好后面的客户端请求应该怎样扩展代码?最简单的方式就是插入循环反复调用 accept 函数,如图: 153 | 154 | ![](https://i.loli.net/2019/01/15/5c3d3c8a283ad.png) 155 | 156 | 可以看出,调用 accept 函数后,紧接着调用 I/O 相关的 read write 函数,然后调用 close 函数。这并非针对服务器套接字,而是针对 accept 函数调用时创建的套接字。 157 | 158 | #### 4.3.2 迭代回声服务器端/客户端 159 | 160 | 程序运行的基本方式: 161 | 162 | - 服务器端在同一时刻只与一个客户端相连,并提供回声服务。 163 | - 服务器端依次向 5 个客户端提供服务并退出。 164 | - 客户端接受用户输入的字符串并发送到服务器端。 165 | - 服务器端将接受的字符串数据传回客户端,即「回声」 166 | - 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。 167 | 168 | 以下是服务端与客户端的代码: 169 | 170 | - [echo_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_server.c) 171 | - [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_client.c) 172 | 173 | 编译: 174 | 175 | ```shell 176 | gcc echo_client.c -o eclient 177 | gcc echo_server.c -o eserver 178 | ``` 179 | 180 | 分别运行: 181 | 182 | ```shell 183 | ./eserver 9190 184 | ./eclient 127.0.0.1 9190 185 | ``` 186 | 187 | 过程和结果: 188 | 189 | 在一个服务端开启后,用另一个终端窗口开启客户端,然后程序会让你输入字符串,然后客户端输入什么字符串,客户端就会返回什么字符串,按 q 退出。这时服务端的运行并没有结束,服务端一共要处理 5 个客户端的连接,所以另外开多个终端窗口同时开启客户端,服务器按照顺序进行处理。 190 | 191 | server: 192 | ![server.png](https://i.loli.net/2019/01/15/5c3d523d0a675.png) 193 | 194 | client: 195 | ![client.png](https://i.loli.net/2019/01/15/5c3d523d336e7.png) 196 | 197 | #### 4.3.3 回声客户端存在的问题 198 | 199 | 以上客户端代码有一个假设「每次调用 read、write函数时都会以字符串为单位执行实际 I/O 操作」 200 | 201 | 但是「第二章」中说过「TCP 不存在数据边界」,上述客户端是基于 TCP 的,因此多次调用 write 函数传递的字符串有可能一次性传递到服务端。此时客户端有可能从服务端收到多个字符串,这不是我们想要的结果。还需要考虑服务器的如下情况: 202 | 203 | 「字符串太长,需要分 2 个包发送!」 204 | 205 | 服务端希望通过调用 1 次 write 函数传输数据,但是如果数据太大,操作系统就有可能把数据分成多个数据包发送到客户端。另外,在此过程中,客户端可能在尚未收到全部数据包时就调用 read 函数。 206 | 207 | 以上的问题都是源自 TCP 的传输特性,解决方法在第 5 章。 208 | 209 | ### 4.4 基于 Windows 的实现 210 | 211 | 暂略 212 | 213 | ### 4.5 习题 214 | 215 | > 答案仅代表本人个人观点,不一定是正确答案。 216 | 217 | 1. **请你说明 TCP/IP 的 4 层协议栈,并说明 TCP 和 UDP 套接字经过的层级结构差异。** 218 | 219 | 答:TCP/IP 的四层协议分为:应用层、TCP/UDP 层、IP层、链路层。差异是一个经过 TCP 层,一个经过 UDP 层。 220 | 221 | 2. **请说出 TCP/IP 协议栈中链路层和IP层的作用,并给出二者关系** 222 | 223 | 答:链路层是物理链接领域标准化的结果,专门定义网络标准。若两台主机通过网络进行数据交换,则首先要做到的就是进行物理链接。IP层:为了在复杂的网络中传输数据,首先需要考虑路径的选择。关系:链路层负责进行一系列物理连接,而IP层负责选择正确可行的物理路径。 224 | 225 | 3. **为何需要把 TCP/IP 协议栈分成 4 层(或7层)?开放式回答。** 226 | 227 | 答:ARPANET 的研制经验表明,对于复杂的计算机网络协议,其结构应该是层次式的。分册的好处:①隔层之间是独立的②灵活性好③结构上可以分隔开④易于实现和维护⑤能促进标准化工作。 228 | 229 | 4. **客户端调用 connect 函数向服务器端发送请求。服务器端调用哪个函数后,客户端可以调用 connect 函数?** 230 | 231 | 答:服务端调用 listen 函数后,客户端可以调用 connect 函数。因为,服务端调用 listen 函数后,服务端套接字才有能力接受请求连接的信号。 232 | 233 | 5. **什么时候创建连接请求等待队列?它有何种作用?与 accept 有什么关系?** 234 | 235 | 答:服务端调用 listen 函数后,accept函数正在处理客户端请求时, 更多的客户端发来了请求连接的数据,此时,就需要创建连接请求等待队列。以便于在accept函数处理完手头的请求之后,按照正确的顺序处理后面正在排队的其他请求。与accept函数的关系:accept函数受理连接请求等待队列中待处理的客户端连接请求。 236 | 237 | 6. **客户端中为何不需要调用 bind 函数分配地址?如果不调用 bind 函数,那何时、如何向套接字分配IP地址和端口号?** 238 | 239 | 答:在调用 connect 函数时分配了地址,客户端IP地址和端口在调用 connect 函数时自动分配,无需调用标记的 bind 函数进行分配。 240 | -------------------------------------------------------------------------------- /ch04/echo_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 1024 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | struct sockaddr_in serv_adr; 17 | 18 | if (argc != 3) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | 24 | sock = socket(PF_INET, SOCK_STREAM, 0); 25 | if (sock == -1) 26 | error_handling("socket() error"); 27 | 28 | memset(&serv_adr, 0, sizeof(serv_adr)); 29 | serv_adr.sin_family = AF_INET; 30 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 31 | serv_adr.sin_port = htons(atoi(argv[2])); 32 | 33 | if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 34 | error_handling("connect() error!"); 35 | else 36 | puts("Connected..........."); 37 | 38 | while (1) 39 | { 40 | fputs("Input message(Q to quit): ", stdout); 41 | fgets(message, BUF_SIZE, stdin); 42 | 43 | if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 44 | break; 45 | 46 | write(sock, message, strlen(message)); 47 | str_len = read(sock, message, BUF_SIZE - 1); 48 | message[str_len] = 0; 49 | printf("Message from server: %s", message); 50 | } 51 | close(sock); 52 | return 0; 53 | } 54 | 55 | void error_handling(char *message) 56 | { 57 | fputs(message, stderr); 58 | fputc('\n', stderr); 59 | exit(1); 60 | } -------------------------------------------------------------------------------- /ch04/echo_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 1024 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sock, clnt_sock; 14 | char message[BUF_SIZE]; 15 | int str_len, i; 16 | 17 | struct sockaddr_in serv_adr, clnt_adr; 18 | socklen_t clnt_adr_sz; 19 | 20 | if (argc != 2) 21 | { 22 | printf("Usage : %s \n", argv[0]); 23 | exit(1); 24 | } 25 | 26 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 27 | if (serv_sock == -1) 28 | error_handling("socket() error"); 29 | 30 | memset(&serv_adr, 0, sizeof(serv_adr)); 31 | serv_adr.sin_family = AF_INET; 32 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 33 | serv_adr.sin_port = htons(atoi(argv[1])); 34 | 35 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 36 | error_handling("bind() error"); 37 | 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | clnt_adr_sz = sizeof(clnt_adr); 42 | //调用 5 次 accept 函数,共为 5 个客户端提供服务 43 | for (i = 0; i < 5; i++) 44 | { 45 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 46 | if (clnt_sock == -1) 47 | error_handling("accept() error"); 48 | else 49 | printf("Connect client %d \n", i + 1); 50 | 51 | while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0) 52 | write(clnt_sock, message, str_len); 53 | 54 | close(clnt_sock); 55 | } 56 | close(serv_sock); 57 | return 0; 58 | } 59 | 60 | void error_handling(char *message) 61 | { 62 | fputs(message, stderr); 63 | fputc('\n', stderr); 64 | exit(1); 65 | } -------------------------------------------------------------------------------- /ch04/hello_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int sock; 12 | struct sockaddr_in serv_addr; 13 | char message[30]; 14 | int str_len; 15 | 16 | if (argc != 3) 17 | { 18 | printf("Usage : %s \n", argv[0]); 19 | exit(1); 20 | } 21 | //创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字 22 | //如果调用 connect 函数,将成为客户端套接字 23 | sock = socket(PF_INET, SOCK_STREAM, 0); 24 | if (sock == -1) 25 | error_handling("socket() error"); 26 | 27 | memset(&serv_addr, 0, sizeof(serv_addr)); 28 | serv_addr.sin_family = AF_INET; 29 | serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 30 | serv_addr.sin_port = htons(atoi(argv[2])); 31 | //调用 connect 函数向服务器发送连接请求 32 | if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 33 | error_handling("connect() error!"); 34 | 35 | str_len = read(sock, message, sizeof(message) - 1); 36 | if (str_len == -1) 37 | error_handling("read() error!"); 38 | 39 | printf("Message from server : %s \n", message); 40 | close(sock); 41 | return 0; 42 | } 43 | 44 | void error_handling(char *message) 45 | { 46 | fputs(message, stderr); 47 | fputc('\n', stderr); 48 | exit(1); 49 | } -------------------------------------------------------------------------------- /ch04/hello_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int serv_sock; 12 | int clnt_sock; 13 | 14 | struct sockaddr_in serv_addr; 15 | struct sockaddr_in clnt_addr; 16 | socklen_t clnt_addr_size; 17 | 18 | char message[] = "Hello World!"; 19 | 20 | if (argc != 2) 21 | { 22 | printf("Usage : %s \n", argv[0]); 23 | exit(1); 24 | } 25 | //调用 socket 函数创建套接字 26 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 27 | if (serv_sock == -1) 28 | error_handling("socket() error"); 29 | 30 | memset(&serv_addr, 0, sizeof(serv_addr)); 31 | serv_addr.sin_family = AF_INET; 32 | serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 33 | serv_addr.sin_port = htons(atoi(argv[1])); 34 | //调用 bind 函数分配ip地址和端口号 35 | if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 36 | error_handling("bind() error"); 37 | //调用 listen 函数将套接字转为可接受连接状态 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | clnt_addr_size = sizeof(clnt_addr); 42 | //调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 43 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); 44 | if (clnt_sock == -1) 45 | error_handling("accept() error"); 46 | //稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求 47 | write(clnt_sock, message, sizeof(message)); 48 | close(clnt_sock); 49 | close(serv_sock); 50 | return 0; 51 | } 52 | 53 | void error_handling(char *message) 54 | { 55 | fputs(message, stderr); 56 | fputc('\n', stderr); 57 | exit(1); 58 | } -------------------------------------------------------------------------------- /ch05/My_op_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #define BUF_SIZE 10240 8 | void error_handling(char *message); 9 | 10 | int main(int argc, char *argv[]) 11 | { 12 | int sock; 13 | char message[BUF_SIZE]; 14 | int str_len; 15 | struct sockaddr_in serv_adr; 16 | if (argc != 3) 17 | { 18 | printf("Usage : %s \n", argv[0]); 19 | exit(1); 20 | } 21 | 22 | sock = socket(PF_INET, SOCK_STREAM, 0); 23 | if (sock == -1) 24 | error_handling("socket() error"); 25 | memset(&serv_adr, 0, sizeof(serv_adr)); 26 | serv_adr.sin_family = AF_INET; 27 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 28 | serv_adr.sin_port = htons(atoi(argv[2])); 29 | 30 | if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 31 | error_handling("connect() error"); 32 | else 33 | printf("连接成功!\n"); 34 | int n, i; 35 | char temp[20]; 36 | puts("请输入你要计算的数字个数:"); 37 | scanf("%d", &n); 38 | sprintf(temp, "%d", n); 39 | strcat(temp, " "); 40 | strcat(message, temp); 41 | for (i = 0; i < n; i++) 42 | { 43 | printf("请输入第 %d 个数字:", i + 1); 44 | scanf("%s", temp); 45 | strcat(temp, " "); 46 | strcat(message, temp); 47 | } 48 | puts("请输入你要进行的运算符(+,-,*):"); 49 | scanf("%s", temp); 50 | strcat(message, temp); 51 | write(sock, message, strlen(message)); 52 | str_len = read(sock, message, BUF_SIZE - 1); 53 | message[str_len] = 0; 54 | printf("运算的结果是: %s\n", message); 55 | return 0; 56 | } 57 | void error_handling(char *message) 58 | { 59 | fputs(message, stderr); 60 | fputc('\n', stderr); 61 | exit(1); 62 | } -------------------------------------------------------------------------------- /ch05/My_op_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 10240 9 | void error_handling(char *message); 10 | 11 | char res[10]; 12 | char *calc(char *s) 13 | { 14 | int len = strlen(s), i; 15 | int n = 0; 16 | for (i = 0; i < len; i++) 17 | if (s[i] == ' ') 18 | { 19 | i++; 20 | break; 21 | } 22 | else 23 | n = n * 10 + (s[i] - '0'); 24 | int *num = malloc(sizeof(int) * n); 25 | int tot = 0, x = 0; 26 | 27 | for (; i < len; i++) 28 | { 29 | if (s[i] == '+' || s[i] == '*' || s[i] == '-') 30 | break; 31 | if (s[i] == ' ') 32 | { 33 | num[tot++] = x; 34 | x = 0; 35 | } 36 | else 37 | x = x * 10 + (s[i] - '0'); 38 | } 39 | int ans = 0; 40 | if (s[i] == '+') 41 | { 42 | for (int i = 0; i < tot; i++) 43 | ans += num[i]; 44 | } 45 | else if (s[i] == '*') 46 | { 47 | ans = 1; 48 | for (int i = 0; i < tot; i++) 49 | ans *= num[i]; 50 | } 51 | else if (s[i] == '-') 52 | { 53 | ans = num[0]; 54 | for (int i = 1; i < tot; i++) 55 | ans -= num[i]; 56 | } 57 | free(num); 58 | sprintf(res, "%d", ans); 59 | return res; 60 | } 61 | int main(int argc, char *argv[]) 62 | { 63 | int serv_sock, clnt_sock; 64 | char message[BUF_SIZE]; 65 | int str_len; 66 | 67 | struct sockaddr_in serv_adr, clnt_adr; 68 | socklen_t clnt_adr_sz; 69 | 70 | if (argc != 2) 71 | { 72 | printf("Usage : %s \n", argv[0]); 73 | exit(1); 74 | } 75 | 76 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 77 | if (serv_sock == -1) 78 | error_handling("socket() error"); 79 | 80 | memset(&serv_adr, 0, sizeof(serv_adr)); 81 | serv_adr.sin_family = AF_INET; 82 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 83 | serv_adr.sin_port = htons(atoi(argv[1])); 84 | 85 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 86 | error_handling("bind() error"); 87 | 88 | if (listen(serv_sock, 5) == -1) 89 | error_handling("listen() error"); 90 | 91 | clnt_adr_sz = sizeof(clnt_adr); 92 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 93 | if (clnt_sock == -1) 94 | error_handling("accept() error"); 95 | str_len = read(clnt_sock, message, BUF_SIZE); 96 | write(clnt_sock, calc(message), str_len); 97 | close(clnt_sock); 98 | close(serv_sock); 99 | return 0; 100 | } 101 | 102 | void error_handling(char *message) 103 | { 104 | fputs(message, stderr); 105 | fputc('\n', stderr); 106 | exit(1); 107 | } -------------------------------------------------------------------------------- /ch05/README.md: -------------------------------------------------------------------------------- 1 | ## 第 5 章 基于 TCP 的服务端/客户端(2) 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | 上一章仅仅是从编程角度学习实现方法,并未详细讨论 TCP 的工作原理。因此,本章将想次讲解 TCP 中必要的理论知识,还将给出第 4 章客户端问题的解决方案。 6 | 7 | ### 5.1 回声客户端的完美实现 8 | 9 | #### 5.1.1 回声服务器没有问题,只有回声客户端有问题? 10 | 11 | 问题不在服务器端,而在客户端,只看代码可能不好理解,因为 I/O 中使用了相同的函数。先回顾一下服务器端的 I/O 相关代码: 12 | 13 | ```c 14 | while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0) 15 | write(clnt_sock, message, str_len); 16 | ``` 17 | 18 | 接着是客户端代码: 19 | 20 | ```c 21 | write(sock, message, strlen(message)); 22 | str_len = read(sock, message, BUF_SIZE - 1); 23 | ``` 24 | 25 | 二者都在循环调用 read 和 write 函数。实际上之前的回声客户端将 100% 接受字节传输的数据,只不过接收数据时的单位有些问题。扩展客户端代码回顾范围,下面是,客户端的代码: 26 | 27 | ```c 28 | while (1) 29 | { 30 | fputs("Input message(Q to quit): ", stdout); 31 | fgets(message, BUF_SIZE, stdin); 32 | 33 | if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 34 | break; 35 | 36 | write(sock, message, strlen(message)); 37 | str_len = read(sock, message, BUF_SIZE - 1); 38 | message[str_len] = 0; 39 | printf("Message from server: %s", message); 40 | } 41 | ``` 42 | 43 | 现在应该理解了问题,回声客户端传输的是字符串,而且是通过调用 write 函数一次性发送的。之后还调用一次 read 函数,期待着接受自己传输的字符串,这就是问题所在。 44 | 45 | #### 5.1.2 回声客户端问题的解决办法 46 | 47 | 这个问题其实很容易解决,因为可以提前接受数据的大小。若之前传输了20字节长的字符串,则再接收时循环调用 read 函数读取 20 个字节即可。既然有了解决办法,那么代码如下: 48 | 49 | - [echo_client2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/echo_client2.c) 50 | 51 | 这样修改为了接收所有传输数据而循环调用 read 函数。测试及运行结果可参考第四章。 52 | 53 | #### 5.1.3 如果问题不在于回声客户端:定义应用层协议 54 | 55 | 回声客户端可以提前知道接收数据的长度,这在大多数情况下是不可能的。那么此时无法预知接收数据长度时应该如何收发数据?这时需要的是**应用层协议**的定义。在收发过程中定好规则(协议)以表示数据边界,或者提前告知需要发送的数据的大小。服务端/客户端实现过程中逐步定义的规则集合就是应用层协议。 56 | 57 | 现在写一个小程序来体验应用层协议的定义过程。要求: 58 | 59 | 1. 服务器从客户端获得多个数组和运算符信息。 60 | 2. 服务器接收到数字候对齐进行加减乘运算,然后把结果传回客户端。 61 | 62 | 例: 63 | 64 | 1. 向服务器传递3,5,9的同事请求加法运算,服务器返回3+5+9的结果 65 | 2. 请求做乘法运算,客户端会收到`3*5*9`的结果 66 | 3. 如果向服务器传递4,3,2的同时要求做减法,则返回4-3-2的运算结果。 67 | 68 | 请自己实现一个程序来实现功能。 69 | 70 | 我自己的实现: 71 | 72 | - [My_op_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/My_op_server.c) 73 | - [My_op_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/My_op_client.c) 74 | 75 | 编译: 76 | 77 | ```shell 78 | gcc My_op_client.c -o myclient 79 | gcc My_op_server.c -o myserver 80 | ``` 81 | 82 | 结果: 83 | 84 | ![](https://i.loli.net/2019/01/15/5c3d966b81c03.png) 85 | 86 | 其实主要是对程序的一点点小改动,只需要在客户端固定好发送的格式,服务端按照固定格式解析,然后返回结果即可。 87 | 88 | 书上的实现: 89 | 90 | - [op_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/op_client.c) 91 | - [op_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/op_server.c) 92 | 93 | 阅读代码要注意一下,`int*`与`char`之间的转换。TCP 中不存在数据边界。 94 | 95 | 编译: 96 | 97 | ```shell 98 | gcc op_client.c -o opclient 99 | gcc op_server.c -o opserver 100 | ``` 101 | 102 | 运行: 103 | 104 | ```shell 105 | ./opserver 9190 106 | ./opclient 127.0.0.1 9190 107 | ``` 108 | 109 | 结果: 110 | 111 | ![](https://i.loli.net/2019/01/16/5c3ea297c7649.png) 112 | 113 | ### 5.2 TCP 原理 114 | 115 | #### 5.2.1 TCP 套接字中的 I/O 缓冲 116 | 117 | TCP 套接字的数据收发无边界。服务器即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器一次性传输了 40 字节,而客户端竟然可以缓慢的分批接受。客户端接受 10 字节后,剩下的 30 字节在何处等候呢? 118 | 119 | 实际上,write 函数调用后并非立即传输数据, read 函数调用后也并非马上接收数据。如图所示,write 函数调用瞬间,数据将移至输出缓冲;read 函数调用瞬间,从输入缓冲读取数据。 120 | 121 | ![](https://i.loli.net/2019/01/16/5c3ea41cd93c6.png) 122 | 123 | I/O 缓冲特性可以整理如下: 124 | 125 | - I/O 缓冲在每个 TCP 套接字中单独存在 126 | - I/O 缓冲在创建套接字时自动生成 127 | - 即使关闭套接字也会继续传递输出缓冲中遗留的数据 128 | - 关闭套接字将丢失输入缓冲中的数据 129 | 130 | 假设发生以下情况,会发生什么事呢? 131 | 132 | > 客户端输入缓冲为 50 字节,而服务器端传输了 100 字节。 133 | 134 | 因为 TCP 不会发生超过输入缓冲大小的数据传输。也就是说,根本不会发生这类问题,因为 TCP 会控制数据流。TCP 中有滑动窗口(Sliding Window)协议,用对话方式如下: 135 | 136 | > - A:你好,最多可以向我传递 50 字节 137 | > - B:好的 138 | > - A:我腾出了 20 字节的空间,最多可以接受 70 字节 139 | > - B:好的 140 | 141 | 数据收发也是如此,因此 TCP 中不会因为缓冲溢出而丢失数据。 142 | 143 | **write 函数在数据传输完成时返回。** 144 | 145 | #### 5.2.2 TCP 内部工作原理 1:与对方套接字的连接 146 | 147 | TCP 套接字从创建到消失所经过的过程分为如下三步: 148 | 149 | - 与对方套接字建立连接 150 | - 与对方套接字进行数据交换 151 | - 断开与对方套接字的连接 152 | 153 | 首先讲解与对方套接字建立连接的过程。连接过程中,套接字的对话如下: 154 | 155 | - 套接字A:你好,套接字 B。我这里有数据给你,建立连接吧 156 | - 套接字B:好的,我这边已就绪 157 | - 套接字A:谢谢你受理我的请求 158 | 159 | TCP 在实际通信中也会经过三次对话过程,因此,该过程又被称为 **Three-way handshaking(三次握手)**。接下来给出连接过程中实际交换的信息方式: 160 | 161 | ![](https://i.loli.net/2019/01/16/5c3ecdec9fc04.png) 162 | 163 | 套接字是全双工方式工作的。也就是说,它可以双向传递数据。因此,收发数据前要做一些准备。首先请求连接的主机 A 要给主机 B 传递以下信息: 164 | 165 | > [SYN] SEQ : 1000 , ACK:- 166 | 167 | 该消息中的 SEQ 为 1000 ,ACK 为空,而 SEQ 为1000 的含义如下: 168 | 169 | > 现在传递的数据包的序号为 1000,如果接收无误,请通知我向您传递 1001 号数据包。 170 | 171 | 这是首次请求连接时使用的消息,又称为 SYN。SYN 是 Synchronization 的简写,表示收发数据前传输的同步消息。接下来主机 B 向 A 传递以下信息: 172 | 173 | > [SYN+ACK] SEQ: 2000, ACK: 1001 174 | 175 | 此时 SEQ 为 2000,ACK 为 1001,而 SEQ 为 2000 的含义如下: 176 | 177 | > 现传递的数据包号为 2000 ,如果接受无误,请通知我向您传递 2001 号数据包。 178 | 179 | 而 ACK 1001 的含义如下: 180 | 181 | > 刚才传输的 SEQ 为 1000 的数据包接受无误,现在请传递 SEQ 为 1001 的数据包。 182 | 183 | 对于主机 A 首次传输的数据包的确认消息(ACK 1001)和为主机 B 传输数据做准备的同步消息(SEQ 2000)捆绑发送。因此,此种类消息又称为 SYN+ACK。 184 | 185 | 收发数据前向数据包分配序号,并向对方通报此序号,这都是为了防止数据丢失做的准备。通过向数据包分配序号并确认,可以在数据包丢失时马上查看并重传丢失的数据包。因此 TCP 可以保证可靠的数据传输。 186 | 187 | 通过这三个过程,这样主机 A 和主机 B 就确认了彼此已经准备就绪。 188 | 189 | #### 5.2.3 TCP 内部工作原理 2:与对方主机的数据交换 190 | 191 | 通过第一步三次握手过程完成了数据交换准备,下面就开始正式收发数据,其默认方式如图所示: 192 | 193 | ![](https://i.loli.net/2019/01/16/5c3ed1a97ce2b.png) 194 | 195 | 图上给出了主机 A 分成 2 个数据包向主机 B 传输 200 字节的过程。首先,主机 A 通过 1 个数据包发送 100 个字节的数据,数据包的 SEQ 为 1200 。主机 B 为了确认这一点,向主机 A 发送 ACK 1301 消息。 196 | 197 | 此时的 ACK 号为 1301 而不是 1201,原因在于 ACK 号的增量为传输的数据字节数。假设每次 ACK 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确 100 个字节全都正确传递还是丢失了一部分,比如只传递了 80 字节。因此按照如下公式传递 ACK 信息: 198 | 199 | > ACK 号 = SEQ 号 + 传递的字节数 + 1 200 | 201 | 与三次握手协议相同,最后 + 1 是为了告知对方下次要传递的 SEQ 号。下面分析传输过程中数据包丢失的情况: 202 | 203 | ![](https://i.loli.net/2019/01/16/5c3ed371187a6.png) 204 | 205 | 上图表示了通过 SEQ 1301 数据包向主机 B 传递 100 字节数据。但中间发生了错误,主机 B 未收到,经过一段时间后,主机 A 仍然未收到对于 SEQ 1301 的 ACK 的确认,因此试着重传该数据包。为了完成该数据包的重传,TCP 套接字启动计时器以等待 ACK 应答。若相应计时器发生超时(Time-out!)则重传。 206 | 207 | #### 5.2.4 TCP 内部工作原理 3:断开套接字的连接 208 | 209 | TCP 套接字的结束过程也非常优雅。如果对方还有数据需要传输时直接断掉该连接会出问题,所以断开连接时需要双方协商,断开连接时双方的对话如下: 210 | 211 | > - 套接字A:我希望断开连接 212 | > - 套接字B:哦,是吗?请稍后。 213 | > - 套接字A:我也准备就绪,可以断开连接。 214 | > - 套接字B:好的,谢谢合作。 215 | 216 | 先由套接字 A 向套接字 B 传递断开连接的信息,套接字 B 发出确认收到的消息,然后向套接字 A 传递可以断开连接的消息,套接字 A 同样发出确认消息。 217 | 218 | ![](https://i.loli.net/2019/01/16/5c3ed7503c18c.png) 219 | 220 | 图中数据包内的 FIN 表示断开连接。也就是说,双方各发送 1 次 FIN 消息后断开连接。此过过程经历 4 个阶段,因此又称四次握手(Four-way handshaking)。SEQ 和 ACK 的含义与之前讲解的内容一致,省略。图中,主机 A 传递了两次 ACK 5001,也许这里会有困惑。其实,第二次 FIN 数据包中的 ACK 5001 只是因为接收了 ACK 消息后未接收到的数据重传的。 221 | 222 | ### 5.3 基于 Windows 的实现 223 | 224 | 暂略 225 | 226 | ### 5.4 习题 227 | 228 | > 答案仅代表本人个人观点,可能不是正确答案。 229 | 230 | 1. **请说明 TCP 套接字连接设置的三次握手过程。尤其是 3 次数据交换过程每次收发的数据内容。** 231 | 232 | 答:TCP套接字的生命周期主要可分为3个部分: ①与对方套接字建立连接 ②与对方套接字进行数据交换 ③断开与对方套接字的连接。 233 | 234 | 其中,在第一步建立连接的阶段,又可细分为3个步骤(即`三次握手`):①由主机1给主机2发送初始的SEQ,首次连接请求是关键字是SYN,表示收发数据前同步传输的消息,此时报文的ACK一般为空。②主机2收到报文以后,给主机 1 传递信息,用一个新的SEQ表示自己的序号,然后ACK代表已经接受到主机1的消息,希望接受下一个消息③主机1收到主机2的确认以后,还需要给主机2给出确认,此时再发送一次SEQ和ACK。 235 | 236 | 2. **TCP 是可靠的数据传输协议,但在通过网络通信的过程中可能丢失数据。请通过 ACK 和 SEQ 说明 TCP 通过和何种机制保证丢失数据的可靠传输。** 237 | 238 | 答:通过超时重传机制来保证,如果报文发出去的特定时间内,发送消息的主机没有收到另一个主机的回复,那么就继续发送这条消息,直到收到回复为止。 239 | 240 | 3. **TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。** 241 | 242 | 答:TCP 套接字调用 write 函数时,数据将移至输出缓冲,在适当的时候,传到对方输入缓冲。这时对方将调用 read 函数从输入缓冲中读取数据。 243 | 244 | 4. **对方主机的输入缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?** 245 | 246 | 答:TCP 中有滑动窗口控制协议,所以传输的时候会保证传输的字节数小于等于自己能接受的字节数。 247 | -------------------------------------------------------------------------------- /ch05/echo_client2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 1024 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | int str_len, recv_len, recv_cnt; 16 | struct sockaddr_in serv_adr; 17 | 18 | if (argc != 3) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | 24 | sock = socket(PF_INET, SOCK_STREAM, 0); 25 | if (sock == -1) 26 | error_handling("socket() error"); 27 | 28 | memset(&serv_adr, 0, sizeof(serv_adr)); 29 | serv_adr.sin_family = AF_INET; 30 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 31 | serv_adr.sin_port = htons(atoi(argv[2])); 32 | 33 | if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 34 | error_handling("connect() error!"); 35 | else 36 | puts("Connected..........."); 37 | 38 | while (1) 39 | { 40 | fputs("Input message(Q to quit): ", stdout); 41 | fgets(message, BUF_SIZE, stdin); 42 | 43 | if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 44 | break; 45 | str_len = write(sock, message, strlen(message)); 46 | 47 | recv_len = 0; 48 | while (recv_len < str_len) 49 | { 50 | recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1); 51 | if (recv_cnt == -1) 52 | error_handling("read() error"); 53 | recv_len += recv_cnt; 54 | } 55 | message[recv_len] = 0; 56 | printf("Message from server: %s", message); 57 | } 58 | close(sock); 59 | return 0; 60 | } 61 | 62 | void error_handling(char *message) 63 | { 64 | fputs(message, stderr); 65 | fputc('\n', stderr); 66 | exit(1); 67 | } -------------------------------------------------------------------------------- /ch05/homework/tcp_client_kehou5.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char* argv[]) 12 | { 13 | int sock; 14 | struct sockaddr_in serv_addr; 15 | 16 | char msg1[]="Hello server!"; 17 | char msg2[]="I'm client."; 18 | char msg3[]="Nice to meet you too!"; 19 | char* str_arr[]={msg1, msg2, msg3}; 20 | char read_buf[100]; 21 | 22 | int str_len, i; 23 | 24 | if(argc!=3){ 25 | printf("Usage : %s \n", argv[0]); 26 | exit(1); 27 | } 28 | 29 | sock=socket(PF_INET, SOCK_STREAM, 0); 30 | if(sock == -1) 31 | error_handling("socket() error"); 32 | 33 | memset(&serv_addr, 0, sizeof(serv_addr)); 34 | serv_addr.sin_family=AF_INET; 35 | serv_addr.sin_addr.s_addr=inet_addr(argv[1]); 36 | serv_addr.sin_port=htons(atoi(argv[2])); 37 | 38 | if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 39 | error_handling("connect() error!"); 40 | 41 | for(i=0; i<3; i++) 42 | { 43 | read(sock, (char*)(&str_len), 4); 44 | read(sock, read_buf, str_len); 45 | puts(read_buf); 46 | 47 | str_len=strlen(str_arr[i])+1; 48 | write(sock, (char*)(&str_len), 4); 49 | write(sock, str_arr[i], str_len); 50 | } 51 | close(sock); 52 | return 0; 53 | } 54 | 55 | void error_handling(char *message) 56 | { 57 | fputs(message, stderr); 58 | fputc('\n', stderr); 59 | exit(1); 60 | } -------------------------------------------------------------------------------- /ch05/homework/tcp_client_kehou6.c: -------------------------------------------------------------------------------- 1 | /***************************************recvsend_clnt.c***************************/ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 30 10 | void error_handling(char *message); 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int sd; 15 | FILE *fp; 16 | 17 | char buf[BUF_SIZE]; 18 | char file_name[BUF_SIZE]; 19 | int read_cnt; 20 | struct sockaddr_in serv_adr; 21 | if(argc!=3) { 22 | printf("Usage: %s \n", argv[0]); 23 | exit(1); 24 | } 25 | 26 | //输入文件名 27 | printf("Input file name: "); 28 | scanf("%s", file_name); 29 | //打开文件名 30 | fp=fopen(file_name, "wb"); 31 | 32 | //创建套接字 33 | sd=socket(PF_INET, SOCK_STREAM, 0); 34 | //初始化 35 | memset(&serv_adr, 0, sizeof(serv_adr)); 36 | serv_adr.sin_family=AF_INET; 37 | serv_adr.sin_addr.s_addr=inet_addr(argv[1]); 38 | serv_adr.sin_port=htons(atoi(argv[2])); 39 | 40 | connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); 41 | //写入要传输的文件 42 | write(sd, file_name, strlen(file_name)+1); 43 | 44 | while((read_cnt=read(sd, buf, BUF_SIZE))!=0) 45 | fwrite((void*)buf, 1, read_cnt, fp); 46 | 47 | fclose(fp); 48 | close(sd); 49 | return 0; 50 | } 51 | 52 | void error_handling(char *message) 53 | { 54 | fputs(message, stderr); 55 | fputc('\n', stderr); 56 | exit(1); 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /ch05/homework/tcp_server_kehou5.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sock; 14 | int clnt_sock; 15 | int str_len, i; 16 | 17 | struct sockaddr_in serv_addr; 18 | struct sockaddr_in clnt_addr; 19 | socklen_t clnt_addr_size; 20 | 21 | char msg1[]="Hello client!"; 22 | char msg2[]="I'm server."; 23 | char msg3[]="Nice to meet you."; 24 | char* str_arr[]={msg1, msg2, msg3}; 25 | char read_buf[100]; 26 | 27 | if(argc!=2){ 28 | printf("Usage : %s \n", argv[0]); 29 | exit(1); 30 | } 31 | 32 | serv_sock=socket(PF_INET, SOCK_STREAM, 0); 33 | if(serv_sock == -1) 34 | error_handling("socket() error"); 35 | 36 | memset(&serv_addr, 0, sizeof(serv_addr)); 37 | serv_addr.sin_family=AF_INET; 38 | serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); 39 | serv_addr.sin_port=htons(atoi(argv[1])); 40 | 41 | if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 42 | error_handling("bind() error"); 43 | 44 | if(listen(serv_sock, 5)==-1) 45 | error_handling("listen() error"); 46 | 47 | clnt_addr_size=sizeof(clnt_addr); 48 | clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size); 49 | if(clnt_sock==-1) 50 | error_handling("accept() error"); 51 | 52 | for(i=0; i<3; i++) 53 | { 54 | //需要写入的字符串的长度 55 | str_len=strlen(str_arr[i])+1; 56 | write(clnt_sock, (char*)(&str_len), 4); 57 | //写入字符串 58 | write(clnt_sock, str_arr[i], str_len); 59 | 60 | //读取的字符串长度 61 | read(clnt_sock, (char*)(&str_len), 4); 62 | //读取的字符串 63 | read(clnt_sock, read_buf, str_len); 64 | puts(read_buf); 65 | } 66 | 67 | close(clnt_sock); 68 | close(serv_sock); 69 | return 0; 70 | } 71 | 72 | void error_handling(char *message) 73 | { 74 | fputs(message, stderr); 75 | fputc('\n', stderr); 76 | exit(1); 77 | } 78 | 79 | -------------------------------------------------------------------------------- /ch05/homework/tcp_server_kehou6.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sd, clnt_sd; 14 | FILE * fp; 15 | char buf[BUF_SIZE]; 16 | char file_name[BUF_SIZE]; 17 | int read_cnt; 18 | 19 | struct sockaddr_in serv_adr, clnt_adr; 20 | socklen_t clnt_adr_sz; 21 | 22 | if(argc!=2) { 23 | printf("Usage: %s \n", argv[0]); 24 | exit(1); 25 | } 26 | 27 | serv_sd=socket(PF_INET, SOCK_STREAM, 0); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family=AF_INET; 31 | serv_adr.sin_addr.s_addr=htonl(INADDR_ANY); 32 | serv_adr.sin_port=htons(atoi(argv[1])); 33 | 34 | bind(serv_sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); 35 | listen(serv_sd, 5); 36 | 37 | clnt_adr_sz=sizeof(clnt_adr); 38 | clnt_sd=accept(serv_sd, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); 39 | 40 | //读取客户端传入的文件名 41 | read(clnt_sd, file_name, BUF_SIZE); 42 | //打开文件 43 | fp=fopen(file_name, "rb"); 44 | //文件存在 45 | if(fp!=NULL) 46 | { 47 | while(1) 48 | { 49 | read_cnt=fread((void*)buf, 1, BUF_SIZE, fp); 50 | if(read_cnt 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #define BUF_SIZE 1024 8 | #define RLT_SIZE 4 //字节大小数 9 | #define OPSZ 4 10 | void error_handling(char *message); 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int sock; 15 | char opmsg[BUF_SIZE]; 16 | int result, opnd_cnt, i; 17 | struct sockaddr_in serv_adr; 18 | if (argc != 3) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | 24 | sock = socket(PF_INET, SOCK_STREAM, 0); 25 | if (sock == -1) 26 | error_handling("socket() error"); 27 | 28 | memset(&serv_adr, 0, sizeof(serv_adr)); 29 | serv_adr.sin_family = AF_INET; 30 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 31 | serv_adr.sin_port = htons(atoi(argv[2])); 32 | 33 | if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 34 | error_handling("connect() error!"); 35 | else 36 | puts("Connected..........."); 37 | 38 | fputs("Operand count: ", stdout); 39 | scanf("%d", &opnd_cnt); 40 | opmsg[0] = (char)opnd_cnt; 41 | 42 | for (i = 0; i < opnd_cnt; i++) 43 | { 44 | printf("Operand %d: ", i + 1); 45 | scanf("%d", (int *)&opmsg[i * OPSZ + 1]); 46 | } 47 | fgetc(stdin); 48 | fputs("Operator: ", stdout); 49 | scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]); 50 | write(sock, opmsg, opnd_cnt * OPSZ + 2); 51 | read(sock, &result, RLT_SIZE); 52 | 53 | printf("Operation result: %d \n", result); 54 | close(sock); 55 | return 0; 56 | } 57 | 58 | void error_handling(char *message) 59 | { 60 | fputs(message, stderr); 61 | fputc('\n', stderr); 62 | exit(1); 63 | } -------------------------------------------------------------------------------- /ch05/op_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #define BUF_SIZE 1024 8 | #define OPSZ 4 9 | void error_handling(char *message); 10 | int calculate(int opnum, int opnds[], char oprator); 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sock, clnt_sock; 14 | char opinfo[BUF_SIZE]; 15 | int result, opnd_cnt, i; 16 | int recv_cnt, recv_len; 17 | struct sockaddr_in serv_adr, clnt_adr; 18 | socklen_t clnt_adr_sz; 19 | if (argc != 2) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | 25 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 26 | if (serv_sock == -1) 27 | error_handling("socket() error"); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 32 | serv_adr.sin_port = htons(atoi(argv[1])); 33 | 34 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 35 | error_handling("bind() error"); 36 | if (listen(serv_sock, 5) == -1) 37 | error_handling("listen() error"); 38 | clnt_adr_sz = sizeof(clnt_adr); 39 | for (int i = 0; i < 5; i++) 40 | { 41 | opnd_cnt = 0; 42 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 43 | read(clnt_sock, &opnd_cnt, 1); 44 | 45 | recv_len = 0; 46 | while ((opnd_cnt * OPSZ + 1) > recv_len) 47 | { 48 | recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE - 1); 49 | recv_len += recv_cnt; 50 | } 51 | result = calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len - 1]); 52 | write(clnt_sock, (char *)&result, sizeof(result)); 53 | close(clnt_sock); 54 | } 55 | close(serv_sock); 56 | return 0; 57 | } 58 | int calculate(int opnum, int opnds[], char op) 59 | { 60 | int result = opnds[0], i; 61 | switch (op) 62 | { 63 | case '+': 64 | for (i = 1; i < opnum; i++) 65 | result += opnds[i]; 66 | break; 67 | case '-': 68 | for (i = 1; i < opnum; i++) 69 | result -= opnds[i]; 70 | break; 71 | case '*': 72 | for (i = 1; i < opnum; i++) 73 | result *= opnds[i]; 74 | break; 75 | } 76 | return result; 77 | } 78 | void error_handling(char *message) 79 | { 80 | fputs(message, stderr); 81 | fputc('\n', stderr); 82 | exit(1); 83 | } -------------------------------------------------------------------------------- /ch06/README.md: -------------------------------------------------------------------------------- 1 | ## 第 6 章 基于 UDP 的服务端/客户端 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | TCP 是内容较多的一个协议,而本章中的 UDP 内容较少,但是也很重要。 6 | 7 | ### 6.1 理解 UDP 8 | 9 | #### 6.1.1 UDP 套接字的特点 10 | 11 | 通过寄信来说明 UDP 的工作原理,这是讲解 UDP 时使用的传统示例,它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式,UDP 也是一种不可靠的数据传输方式。 12 | 13 | 因为 UDP 没有 TCP 那么复杂,所以编程难度比较小,性能也比 TCP 高。在更重视性能的情况下可以选择 UDP 的传输方式。 14 | 15 | TCP 与 UDP 的区别很大一部分来源于流控制。也就是说 TCP 的生命在于流控制。 16 | 17 | #### 6.1.2 UDP 的工作原理 18 | 19 | 如图所示: 20 | 21 | ![](https://i.loli.net/2019/01/17/5c3fd29c70bf2.png) 22 | 23 | 从图中可以看出,IP 的作用就是让离开主机 B 的 UDP 数据包准确传递到主机 A 。但是把 UDP 数据包最终交给主机 A 的某一 UDP 套接字的过程是由 UDP 完成的。UDP 的最重要的作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字。 24 | 25 | #### 6.1.3 UDP 的高效使用 26 | 27 | UDP 也具有一定的可靠性。对于通过网络实时传递的视频或者音频时情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点: 28 | 29 | - 收发数据前后进行的连接设置及清除过程。 30 | - 收发过程中为保证可靠性而添加的流控制。 31 | 32 | 如果收发的数据量小但是需要频繁连接时,UDP 比 TCP 更高效。 33 | 34 | ### 6.2 实现基于 UDP 的服务端/客户端 35 | 36 | #### 6.2.1 UDP 中的服务端和客户端没有连接 37 | 38 | UDP 中的服务端和客户端不像 TCP 那样在连接状态下交换数据,因此与 TCP 不同,无需经过连接过程。也就是说,不必调用 TCP 连接过程中调用的 listen 和 accept 函数。UDP 中只有创建套接字和数据交换的过程。 39 | 40 | #### 6.2.2 UDP 服务器和客户端均只需一个套接字 41 | 42 | TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端提供服务,除了守门的服务器套接字之外,还需要 10 个服务器套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。只需要一个 UDP 套接字就可以向任意主机传输数据,如图所示: 43 | 44 | ![](https://i.loli.net/2019/01/17/5c3fd703f3c40.png) 45 | 46 | 图中展示了 1 个 UDP 套接字与 2 个不同主机交换数据的过程。也就是说,只需 1 个 UDP 套接字就能和多台主机进行通信。 47 | 48 | #### 6.2.3 基于 UDP 的数据 I/O 函数 49 | 50 | 创建好 TCP 套接字以后,传输数据时无需加上地址信息。因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字知道目标地址信息。但 UDP 套接字不会保持连接状态(UDP 套接字只有简单的邮筒功能),因此每次传输数据时都需要添加目标的地址信息。这相当于寄信前在信件中填写地址。接下来是 UDP 的相关函数: 51 | 52 | ```c 53 | #include 54 | ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, 55 | struct sockaddr *to, socklen_t addrlen); 56 | /* 57 | 成功时返回发送的字节数,失败时返回 -1 58 | sock: 用于传输数据的 UDP 套接字 59 | buff: 保存待传输数据的缓冲地址值 60 | nbytes: 待传输的数据长度,以字节为单位 61 | flags: 可选项参数,若没有则传递 0 62 | to: 存有目标地址的 sockaddr 结构体变量的地址值 63 | addrlen: 传递给参数 to 的地址值结构体变量长度 64 | */ 65 | ``` 66 | 67 | 上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。UDP 数据的发送并不固定,因此该函数定义为可接受发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息。 68 | 69 | ```c 70 | #include 71 | ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, 72 | struct sockaddr *from, socklen_t *addrlen); 73 | /* 74 | 成功时返回接收的字节数,失败时返回 -1 75 | sock: 用于传输数据的 UDP 套接字 76 | buff: 保存待传输数据的缓冲地址值 77 | nbytes: 待传输的数据长度,以字节为单位 78 | flags: 可选项参数,若没有则传递 0 79 | from: 存有发送端地址信息的 sockaddr 结构体变量的地址值 80 | addrlen: 保存参数 from 的结构体变量长度的变量地址值。 81 | */ 82 | ``` 83 | 84 | 编写 UDP 程序的最核心的部分就在于上述两个函数,这也说明二者在 UDP 数据传输中的地位。 85 | 86 | #### 6.2.4 基于 UDP 的回声服务器端/客户端 87 | 88 | 下面是实现的基于 UDP 的回声服务器的服务器端和客户端: 89 | 90 | 代码: 91 | 92 | - [uecho_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_client.c) 93 | - [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 94 | 95 | 编译运行: 96 | 97 | ```shell 98 | gcc uecho_client.c -o uclient 99 | gcc uecho_server.c -o userver 100 | ./server 9190 101 | ./uclient 127.0.0.1 9190 102 | ``` 103 | 104 | 结果: 105 | 106 | ![](https://i.loli.net/2019/01/17/5c3feb85baa83.png) 107 | 108 | TCP 客户端套接字在调用 connect 函数时自动分配IP地址和端口号,既然如此,UDP 客户端何时分配IP地址和端口号? 109 | 110 | #### 6.2.5 UDP 客户端套接字的地址分配 111 | 112 | 仔细观察 UDP 客户端可以发现,UDP 客户端缺少了把IP和端口分配给套接字的过程。TCP 客户端调用 connect 函数自动完成此过程,而 UDP 中连能承担相同功能的函数调用语句都没有。究竟在什么时候分配IP和端口号呢? 113 | 114 | UDP 程序中,调用 sendto 函数传输数据前应该完成对套接字的地址分配工作,因此调用 bind 函数。当然,bind 函数在 TCP 程序中出现过,但 bind 函数不区分 TCP 和 UDP,也就是说,在 UDP 程序中同样可以调用。另外,如果调用 sendto 函数尚未分配地址信息,则在首次调用 sendto 函数时给相应套接字自动分配 IP 和端口。而且此时分配的地址一直保留到程序结束为止,因此也可以用来和其他 UDP 套接字进行数据交换。当然,IP 用主机IP,端口号用未选用的任意端口号。 115 | 116 | 综上所述,调用 sendto 函数时自动分配IP和端口号,因此,UDP 客户端中通常无需额外的地址分配过程。所以之前的示例中省略了该过程。这也是普遍的实现方式。 117 | 118 | ### 6.3 UDP 的数据传输特性和调用 connect 函数 119 | 120 | #### 6.3.1 存在数据边界的 UDP 套接字 121 | 122 | 前面说得 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」 123 | 124 | 相反,UDP 是具有数据边界的下一,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数应该完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证: 125 | 126 | - [bound_host1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host1.c) 127 | - [bound_host2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host2.c) 128 | 129 | 编译运行: 130 | 131 | ```shell 132 | gcc bound_host1.c -o host1 133 | gcc bound_host2.c -o host2 134 | ./host1 9190 135 | ./host2 127.0.0.1 9190 136 | ``` 137 | 138 | 运行结果: 139 | 140 | ![](https://i.loli.net/2019/01/17/5c3ff966a8d34.png) 141 | 142 | host1 是服务端,host2 是客户端,host2 一次性把数据发给服务端后,结束程序。但是因为服务端每隔五秒才接收一次,所以服务端每隔五秒接收一次消息。 143 | 144 | **从运行结果也可以证明 UDP 通信过程中 I/O 的调用次数必须保持一致** 145 | 146 | #### 6.3.2 已连接(connect)UDP 套接字与未连接(unconnected)UDP 套接字 147 | 148 | TCP 套接字中需注册待传传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段: 149 | 150 | - 第 1 阶段:向 UDP 套接字注册目标 IP 和端口号 151 | - 第 2 阶段:传输数据 152 | - 第 3 阶段:删除 UDP 套接字中注册的目标地址信息。 153 | 154 | 每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接 connected 套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。 155 | 156 | #### 6.3.3 创建已连接 UDP 套接字 157 | 158 | 创建已连接 UDP 套接字过程格外简单,只需针对 UDP 套接字调用 connect 函数。 159 | 160 | ```c 161 | sock = socket(PF_INET, SOCK_DGRAM, 0); 162 | memset(&adr, 0, sizeof(adr)); 163 | adr.sin_family = AF_INET; 164 | adr.sin_addr.s_addr = inet_addr(argv[1]); 165 | adr.sin_port = htons(atoi(argv[2])); 166 | connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 167 | ``` 168 | 169 | 上述代码看似与 TCP 套接字创建过程一致,但 socket 函数的第二个参数分明是 SOCK_DGRAM 。也就是说,创建的的确是 UDP 套接字。当然针对 UDP 调用 connect 函数并不是意味着要与对方 UDP 套接字连接,这只是向 UDP 套接字注册目标IP和端口信息。 170 | 171 | 之后就与 TCP 套接字一致,每次调用 sendto 函数时只需传递信息数据。因为已经指定了收发对象,所以不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信。 172 | 173 | 下面的例子把之前的 [uecho_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_client.c) 程序改成了基于已连接 UDP 的套接字的程序,因此可以结合 [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 程序运行。代码如下: 174 | 175 | - [uecho_con_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_con_client.c) 176 | 177 | 编译运行过程与上面一样,故省略。 178 | 179 | 上面的代码中用 write、read 函数代替了 sendto、recvfrom 函数。 180 | 181 | ### 6.4 基于 Windows 的实现 182 | 183 | 暂略 184 | 185 | ### 6.5 习题 186 | 187 | > 以下答案仅代表本人个人观点,可能不是正确答案。 188 | 189 | 1. **UDP 为什么比 TCP 快?为什么 TCP 传输可靠而 TCP 传输不可靠?** 190 | 191 | 答:为了提供可靠的数据传输服务,TCP 在不可靠的IP层进行流控制,而 UDP 缺少这种流控制。所以 UDP 是不可靠的连接。 192 | 193 | 2. **下面不属于 UDP 特点的是?** 194 | 195 | 下面加粗的代表此句话正确 196 | 197 | 1. **UDP 不同于 TCP ,不存在连接概念,所以不像 TCP 那样只能进行一对一的数据传输。** 198 | 2. 利用 UDP 传输数据时,如果有 2 个目标,则需要 2 个套接字。 199 | 3. UDP 套接字中无法使用已分配给 TCP 的同一端口号 200 | 4. **UDP 套接字和 TCP 套接字可以共存。若需要,可以同时在同一主机进行 TCP 和 UDP 数据传输。** 201 | 5. 针对 UDP 函数也可以调用 connect 函数,此时 UDP 套接字跟 TCP 套接字相同,也需要经过 3 次握手阶段。 202 | 203 | 3. **UDP 数据报向对方主机的 UDP 套接字传递过程中,IP 和 UDP 分别负责哪些部分?** 204 | 205 | 答:IP的作用就是让离开主机的 UDP 数据包准确传递到另一个主机。但把 UDP 包最终交给主机的某一 UDP 套接字的过程则是由 UDP 完成的。UDP 的最重要的作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字。 206 | 207 | 4. **UDP 一般比 TCP 快,但根据交换数据的特点,其差异可大可小。请你说明何种情况下 UDP 的性能优于 TCP?** 208 | 209 | 答:如果收发数据量小但需要频繁连接时,UDP 比 TCP 更高效。 210 | 211 | 5. **客户端 TCP 套接字调用 connect 函数时自动分配IP和端口号。UDP 中不调用 bind 函数,那何时分配IP和端口号?** 212 | 213 | 答:在首次调用 sendto 函数时自动给相应的套接字分配IP和端口号。而且此时分配的地址一直保留到程序结束为止。 214 | 215 | 6. **TCP 客户端必须调用 connect 函数,而 UDP 可以选择性调用。请问,在 UDP 中调用 connect 函数有哪些好处?** 216 | 217 | 答:要与同一个主机进行长时间通信时,将 UDP 套接字变成已连接套接字会提高效率。因为三个阶段中,第一个阶段和第三个阶段占用了一大部分时间,调用 connect 函数可以节省这些时间。 218 | -------------------------------------------------------------------------------- /ch06/bound_host1.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | struct sockaddr_in my_adr, your_adr; 16 | socklen_t adr_sz; 17 | int str_len, i; 18 | if (argc != 2) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | sock = socket(PF_INET, SOCK_DGRAM, 0); 24 | if (sock == -1) 25 | error_handling("socket() error"); 26 | 27 | memset(&my_adr, 0, sizeof(my_adr)); 28 | my_adr.sin_family = AF_INET; 29 | my_adr.sin_addr.s_addr = htonl(INADDR_ANY); 30 | my_adr.sin_port = htons(atoi(argv[1])); 31 | 32 | if (bind(sock, (struct sockaddr *)&my_adr, sizeof(my_adr)) == -1) 33 | error_handling("bind() error"); 34 | 35 | for (i = 0; i < 3; i++) 36 | { 37 | sleep(5); 38 | adr_sz = sizeof(your_adr); 39 | str_len = recvfrom(sock, message, BUF_SIZE, 0, 40 | (struct sockaddr *)&your_adr, &adr_sz); 41 | printf("Message %d: %s \n", i + 1, message); 42 | } 43 | close(sock); 44 | return 0; 45 | } 46 | 47 | void error_handling(char *message) 48 | { 49 | fputs(message, stderr); 50 | fputc('\n', stderr); 51 | exit(1); 52 | } -------------------------------------------------------------------------------- /ch06/bound_host2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char msg1[] = "Hi!"; 15 | char msg2[] = "I'm another UDP host!"; 16 | char msg3[] = "Nice to meet you"; 17 | 18 | struct sockaddr_in your_adr; 19 | socklen_t your_adr_sz; 20 | if (argc != 3) 21 | { 22 | printf("Usage : %s \n", argv[0]); 23 | exit(1); 24 | } 25 | sock = socket(PF_INET, SOCK_DGRAM, 0); 26 | if (sock == -1) 27 | error_handling("socket() error"); 28 | memset(&your_adr, 0, sizeof(your_adr)); 29 | your_adr.sin_family = AF_INET; 30 | your_adr.sin_addr.s_addr = inet_addr(argv[1]); 31 | your_adr.sin_port = htons(atoi(argv[2])); 32 | 33 | sendto(sock, msg1, sizeof(msg1), 0, 34 | (struct sockaddr *)&your_adr, sizeof(your_adr)); 35 | sendto(sock, msg2, sizeof(msg2), 0, 36 | (struct sockaddr *)&your_adr, sizeof(your_adr)); 37 | sendto(sock, msg3, sizeof(msg3), 0, 38 | (struct sockaddr *)&your_adr, sizeof(your_adr)); 39 | close(sock); 40 | return 0; 41 | } 42 | 43 | void error_handling(char *message) 44 | { 45 | fputs(message, stderr); 46 | fputc('\n', stderr); 47 | exit(1); 48 | } 49 | -------------------------------------------------------------------------------- /ch06/homework/uchar_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char* message); 10 | 11 | int main(int argc, char* argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | socklen_t adr_sz; 17 | 18 | struct sockaddr_in serv_adr, from_adr; 19 | if(argc != 3) { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | 24 | sock = socket(PF_INET, SOCK_DGRAM, 0); 25 | if(sock == -1) 26 | error_handling("socket() error"); 27 | 28 | memset(&serv_adr, 0, sizeof(serv_adr)); 29 | serv_adr.sin_family = AF_INET; 30 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 31 | serv_adr.sin_port = htons(atoi(argv[2])); 32 | 33 | while(1) 34 | { 35 | fputs("Inset message(q to Quit): ", stdout); 36 | fgets(message, sizeof(message), stdin); 37 | if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 38 | break; 39 | 40 | sendto(sock, message, BUF_SIZE, 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); 41 | adr_sz = sizeof(from_adr); 42 | str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz); 43 | 44 | message[str_len] = 0; 45 | printf("Message from server: %s", message); 46 | } 47 | close(sock); 48 | return 0; 49 | } 50 | 51 | void error_handling(char* message) 52 | { 53 | fputs(message, stderr); 54 | fputc('\n', stderr); 55 | exit(1); 56 | } -------------------------------------------------------------------------------- /ch06/homework/uchar_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char* message); 10 | 11 | int main(int argc, char* argv[]) 12 | { 13 | int serv_sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | socklen_t clnt_adr_sz; 17 | 18 | struct sockaddr_in serv_adr, clnt_adr; 19 | if(argc != 2) { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | 24 | serv_sock = socket(PF_INET, SOCK_DGRAM, 0); 25 | if(serv_sock == -1) 26 | error_handling("socket() error"); 27 | 28 | memset(&serv_adr, 0, sizeof(serv_adr)); 29 | serv_adr.sin_family = AF_INET; 30 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 31 | serv_adr.sin_port = htons(atoi(argv[1])); 32 | 33 | if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) 34 | error_handling("bind() error"); 35 | 36 | clnt_adr_sz = sizeof(clnt_adr); 37 | while(1) 38 | { 39 | str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); 40 | message[str_len] = 0; 41 | printf("Message from client: %s\n", message); 42 | 43 | fputs("Insert message(q to Quit): ", stdout); 44 | fgets(message, sizeof(message), stdin); 45 | if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 46 | break; 47 | sendto(serv_sock, message, strlen(message), 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz); 48 | } 49 | close(serv_sock); 50 | return 0; 51 | } 52 | 53 | void error_handling(char* message) 54 | { 55 | fputs(message, stderr); 56 | fputc('\n', stderr); 57 | exit(1); 58 | } -------------------------------------------------------------------------------- /ch06/uecho_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | socklen_t adr_sz; 17 | 18 | struct sockaddr_in serv_adr, from_adr; 19 | if (argc != 3) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | //创建 UDP 套接字 25 | sock = socket(PF_INET, SOCK_DGRAM, 0); 26 | if (sock == -1) 27 | error_handling("socket() error"); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 32 | serv_adr.sin_port = htons(atoi(argv[2])); 33 | 34 | while (1) 35 | { 36 | fputs("Insert message(q to quit): ", stdout); 37 | fgets(message, sizeof(message), stdin); 38 | if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 39 | break; 40 | //向服务器传输数据,会自动给自己分配IP地址和端口号 41 | sendto(sock, message, strlen(message), 0, 42 | (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 43 | adr_sz = sizeof(from_adr); 44 | str_len = recvfrom(sock, message, BUF_SIZE, 0, 45 | (struct sockaddr *)&from_adr, &adr_sz); 46 | message[str_len] = 0; 47 | printf("Message from server: %s", message); 48 | } 49 | close(sock); 50 | return 0; 51 | } 52 | 53 | void error_handling(char *message) 54 | { 55 | fputs(message, stderr); 56 | fputc('\n', stderr); 57 | exit(1); 58 | } 59 | -------------------------------------------------------------------------------- /ch06/uecho_con_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | socklen_t adr_sz; //多余变量 17 | 18 | struct sockaddr_in serv_adr, from_adr; //不需要 from_adr 19 | if (argc != 3) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | //创建 UDP 套接字 25 | sock = socket(PF_INET, SOCK_DGRAM, 0); 26 | if (sock == -1) 27 | error_handling("socket() error"); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 32 | serv_adr.sin_port = htons(atoi(argv[2])); 33 | 34 | connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 35 | 36 | while (1) 37 | { 38 | fputs("Insert message(q to quit): ", stdout); 39 | fgets(message, sizeof(message), stdin); 40 | if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 41 | break; 42 | //向服务器传输数据,会自动给自己分配IP地址和端口号 43 | 44 | /* 45 | sendto(sock, message, strlen(message), 0, 46 | (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 47 | */ 48 | write(sock, message, strlen(message)); 49 | /* 50 | adr_sz = sizeof(from_adr); 51 | str_len = recvfrom(sock, message, BUF_SIZE, 0, 52 | (struct sockaddr *)&from_adr, &adr_sz); 53 | */ 54 | str_len = read(sock, message, sizeof(message) - 1); 55 | message[str_len] = 0; 56 | printf("Message from server: %s", message); 57 | } 58 | close(sock); 59 | return 0; 60 | } 61 | 62 | void error_handling(char *message) 63 | { 64 | fputs(message, stderr); 65 | fputc('\n', stderr); 66 | exit(1); 67 | } 68 | -------------------------------------------------------------------------------- /ch06/uecho_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | socklen_t clnt_adr_sz; 17 | 18 | struct sockaddr_in serv_adr, clnt_adr; 19 | if (argc != 2) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | //创建 UDP 套接字后,向 socket 的第二个参数传递 SOCK_DGRAM 25 | serv_sock = socket(PF_INET, SOCK_DGRAM, 0); 26 | if (serv_sock == -1) 27 | error_handling("UDP socket creation eerror"); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 32 | serv_adr.sin_port = htons(atoi(argv[1])); 33 | //分配地址接受数据,不限制数据传输对象 34 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 35 | error_handling("bind() error"); 36 | 37 | while (1) 38 | { 39 | clnt_adr_sz = sizeof(clnt_adr); 40 | str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, 41 | (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 42 | //通过上面的函数调用同时获取数据传输端的地址。正是利用该地址进行逆向重传 43 | sendto(serv_sock, message, str_len, 0, 44 | (struct sockaddr *)&clnt_adr, clnt_adr_sz); 45 | } 46 | close(serv_sock); 47 | return 0; 48 | } 49 | 50 | void error_handling(char *message) 51 | { 52 | fputs(message, stderr); 53 | fputc('\n', stderr); 54 | exit(1); 55 | } -------------------------------------------------------------------------------- /ch07/README.md: -------------------------------------------------------------------------------- 1 | ## 第 7 章 优雅的断开套接字的连接 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | 本章讨论如何优雅的断开套接字的连接,之前用的方法不够优雅是因为,我们是调用 close 函数或 closesocket 函数单方面断开连接的。 6 | 7 | ### 7.1 基于 TCP 的半关闭 8 | 9 | TCP 的断开连接过程比建立连接更重要,因为连接过程中一般不会出现大问题,但是断开过程可能发生预想不到的情况。因此应该准确掌控。所以要**掌握半关闭(Half-close)**,才能明确断开过程。 10 | 11 | #### 7.1.1 单方面断开连接带来的问题 12 | 13 | Linux 和 Windows 的 closesocket 函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接收数据。因此在某些情况下,通信一方单方面的断开套接字连接,显得不太优雅。如图所示: 14 | 15 | ![](https://i.loli.net/2019/01/18/5c412a8baa2d8.png) 16 | 17 | 图中描述的是 2 台主机正在进行双向通信,主机 A 发送完最后的数据后,调用 close 函数断开了最后的连接,之后主机 A 无法再接受主机 B 传输的数据。实际上,是完全无法调用与接受数据相关的函数。最终,由主机 B 传输的、主机 A 必须要接受的数据也销毁了。 18 | 19 | 为了解决这类问题,「只关闭一部分数据交换中使用的流」的方法应运而生。断开一部分连接是指,可以传输数据但是无法接收,或可以接受数据但无法传输。顾名思义就是只关闭流的一半。 20 | 21 | #### 7.1.2 套接字和流(Stream) 22 | 23 | 两台主机通过套接字建立连接后进入可交换数据的状态,又称「流形成的状态」。也就是把建立套接字后可交换数据的状态看作一种流。 24 | 25 | 此处的流可以比作水流。水朝着一个方向流动,同样,在套接字的流中,数据也止呕能向一个方向流动。因此,为了进行双向通信,需要如图所示的两个流: 26 | 27 | ![](https://i.loli.net/2019/01/18/5c412c3ba25dd.png) 28 | 29 | 一旦两台主机之间建立了套接字连接,每个主机就会拥有单独的输入流和输出流。当然,其中一个主机的输入流与另一个主机的输出流相连,而输出流则与另一个主机的输入流相连。另外,本章讨论的「优雅的断开连接方式」只断开其中 1 个流,而非同时断开两个流。Linux 和 Windows 的 closesocket 函数将同时断开这两个流,因此与「优雅」二字还有一段距离。 30 | 31 | #### 7.1.3 针对优雅断开的 shutdown 函数 32 | 33 | shutdown 用来关闭其中一个流: 34 | 35 | ```c 36 | #include 37 | int shutdown(int sock, int howto); 38 | /* 39 | 成功时返回 0 ,失败时返回 -1 40 | sock: 需要断开套接字文件描述符 41 | howto: 传递断开方式信息 42 | */ 43 | ``` 44 | 45 | 调用上述函数时,第二个参数决定断开连接的方式,其值如下所示: 46 | 47 | - `SHUT_RD` : 断开输入流 48 | - `SHUT_WR` : 断开输出流 49 | - `SHUT_RDWR` : 同时断开 I/O 流 50 | 51 | 若向 shutdown 的第二个参数传递`SHUT_RD`,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用相关函数。如果向 shutdown 的第二个参数传递`SHUT_WR`,则中断输出流,也就无法传输数据。若如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字`SHUT_RDWR`,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以`SHUT_RD`为参数,另一次以`SHUT_WR`为参数。 52 | 53 | #### 7.1.4 为何要半关闭 54 | 55 | 考虑以下情况: 56 | 57 | > 一旦客户端连接到服务器,服务器将约定的文件传输给客户端,客户端收到后发送字符串「Thank you」给服务器端。 58 | 59 | 此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序的还嫌难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止的调用输入函数,因为这有可能导致程序**阻塞**。 60 | 61 | > 是否可以让服务器和客户端约定一个代表文件尾的字符? 62 | 63 | 这种方式也有问题,因为这意味这文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接受 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF ? 64 | 65 | > 断开输出流时向主机传输 EOF。 66 | 67 | 当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。 68 | 69 | #### 7.1.5 基于半关闭的文件传输程序 70 | 71 | 上述文件传输服务器端和客户端的数据流可以整理如图: 72 | 73 | ![](https://i.loli.net/2019/01/18/5c41326280ab5.png) 74 | 75 | 下面的代码为编程简便,省略了大量错误处理代码。 76 | 77 | - [file_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch07/file_client.c) 78 | - [file_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch07/file_server.c) 79 | 80 | 编译运行: 81 | 82 | ```shell 83 | gcc file_client.c -o fclient 84 | gcc file_server.c -o fserver 85 | ./fserver 9190 86 | ./fclient 127.0.0.1 9190 87 | ``` 88 | 89 | 结果: 90 | 91 | ![](https://i.loli.net/2019/01/18/5c4140bc8db2f.png) 92 | 93 | 客户端接受完成后,服务器会接收到来自客户端的感谢信息。 94 | 95 | ### 7.2 基于 Windows 的实现 96 | 97 | 暂略 98 | 99 | ### 7.3 习题 100 | 101 | > 以下答案仅代表本人个人观点,可能不是正确答案 102 | 103 | 1. **解释 TCP 中「流」的概念。UDP 中能否形成流?请说明原因。** 104 | 105 | 答:两台主机中通过套接字建立连接后进入可交换数据的状态,又称「流形成的状态」。也就是把建立套接字后可交换数据的状态看做一种流。UDP 没有建立连接的过程,所以不能形成流。 106 | 107 | 2. **Linux 中的 close 函数或 Windows 中的 closesocket 函数属于单方面断开连接的方法,有可能带来一些问题。什么是单方面断开连接?什么情形下会出现问题?** 108 | 109 | 答:单方面断开连接就是两台主机正在通信,其中一台主机关闭了所有连接,那么一台主机向另一台主机传输的数据可能会没有接收到而损毁。传输文件的服务器只需连续传输文件数据即可,而客户端不知道需要接收数据到何时。客户端也没有办法无休止的调用输入函数。现在需要一个 EOF 代表数据已经传输完毕,那么这时就需要半关闭,服务端把自己的输出流关了,这时客户端就知数据已经传输完毕,因为服务端的输入流还没关,客户端可以给服务器汇报,接收完毕。 110 | 111 | 3. **什么是半关闭?针对输出流执行半关闭的主机处于何种状态?半关闭会导致对方主机接收什么消息?** 112 | 113 | 答:半关闭就是把输入流或者输出流关了。针对输出流执行半关闭的主机处于可以接收数据而不能发送数据。半关闭会导致对方主机接收一个 EOF 文件结束符。对方就知道你的数据已经传输完毕。 114 | -------------------------------------------------------------------------------- /ch07/file_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sd; 14 | FILE *fp; 15 | 16 | char buf[BUF_SIZE]; 17 | int read_cnt; 18 | struct sockaddr_in serv_adr; 19 | 20 | if (argc != 3) 21 | { 22 | printf("Usage : %s \n", argv[0]); 23 | exit(1); 24 | } 25 | 26 | fp = fopen("receive.cpp", "wb"); 27 | sd = socket(PF_INET, SOCK_STREAM, 0); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 32 | serv_adr.sin_port = htons(atoi(argv[2])); 33 | 34 | connect(sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 35 | 36 | while ((read_cnt = read(sd, buf, BUF_SIZE)) != 0) 37 | fwrite((void *)buf, 1, read_cnt, fp); 38 | 39 | puts("Received file data"); 40 | write(sd, "Thank you", 10); 41 | fclose(fp); 42 | close(sd); 43 | return 0; 44 | } 45 | 46 | void error_handling(char *message) 47 | { 48 | fputs(message, stderr); 49 | fputc('\n', stderr); 50 | exit(1); 51 | } -------------------------------------------------------------------------------- /ch07/file_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sd, clnt_sd; 14 | FILE *fp; 15 | char buf[BUF_SIZE]; 16 | int read_cnt; 17 | 18 | struct sockaddr_in serv_adr, clnt_adr; 19 | socklen_t clnt_adr_sz; 20 | 21 | if (argc != 2) 22 | { 23 | printf("Usage : %s \n", argv[0]); 24 | exit(1); 25 | } 26 | fp = fopen("file_server.c", "rb"); 27 | serv_sd = socket(PF_INET, SOCK_STREAM, 0); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 32 | serv_adr.sin_port = htons(atoi(argv[1])); 33 | 34 | bind(serv_sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 35 | listen(serv_sd, 5); 36 | 37 | clnt_adr_sz = sizeof(clnt_adr); 38 | clnt_sd = accept(serv_sd, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 39 | 40 | while (1) 41 | { 42 | //从文件流中读取数据,buffer为接收数据的地址,size为一个单元的大小,count为单元个数,stream为文件流 43 | //返回实际读取的单元个数 44 | read_cnt = fread((void *)buf, 1, BUF_SIZE, fp); 45 | if (read_cnt < BUF_SIZE) 46 | { 47 | write(clnt_sd, buf, read_cnt); 48 | break; 49 | } 50 | write(clnt_sd, buf, BUF_SIZE); 51 | } 52 | 53 | shutdown(clnt_sd, SHUT_WR); 54 | read(clnt_sd, buf, BUF_SIZE); 55 | printf("Message from client: %s \n", buf); 56 | 57 | fclose(fp); 58 | close(clnt_sd); 59 | close(serv_sd); 60 | return 0; 61 | } 62 | 63 | void error_handling(char *message) 64 | { 65 | fputs(message, stderr); 66 | fputc('\n', stderr); 67 | exit(1); 68 | } -------------------------------------------------------------------------------- /ch08/README.md: -------------------------------------------------------------------------------- 1 | ## 第 8 章 域名及网络地址 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | ### 8.1 域名系统 6 | 7 | DNS 是对IP地址和域名进行相互转换的系统,其核心是 DNS 服务器 8 | 9 | #### 8.1.1 什么是域名 10 | 11 | 域名就是我们常常在地址栏里面输入的地址,将比较难记忆的IP地址变成人类容易理解的信息。 12 | 13 | #### 8.1.2 DNS 服务器 14 | 15 | 相当于一个字典,可以查询出某一个域名对应的IP地址 16 | 17 | ![](https://i.loli.net/2019/01/18/5c41854859ae3.png) 18 | 19 | 如图所示,显示了 DNS 服务器的查询路径。 20 | 21 | ### 8.2 IP地址和域名之间的转换 22 | 23 | #### 8.2.1 程序中有必要使用域名吗? 24 | 25 | 一句话,需要,因为IP地址可能经常改变,而且也不容易记忆,通过域名可以随时更改解析,达到更换IP的目的 26 | 27 | #### 8.2.2 利用域名获取IP地址 28 | 29 | 使用以下函数可以通过传递字符串格式的域名获取IP地址 30 | 31 | ```c 32 | #include 33 | struct hostent *gethostbyname(const char *hostname); 34 | /* 35 | 成功时返回 hostent 结构体地址,失败时返回 NULL 指针 36 | */ 37 | ``` 38 | 39 | 这个函数使用方便,只要传递字符串,就可以返回域名对应的IP地址。只是返回时,地址信息装入 hostent 结构体。此结构体的定义如下: 40 | 41 | ```c 42 | struct hostent 43 | { 44 | char *h_name; /* Official name of host. */ 45 | char **h_aliases; /* Alias list. */ 46 | int h_addrtype; /* Host address type. */ 47 | int h_length; /* Length of address. */ 48 | char **h_addr_list; /* List of addresses from name server. */ 49 | }; 50 | ``` 51 | 52 | 从上述结构体可以看出,不止返回IP信息,同事还带着其他信息一起返回。域名转换成IP时只需要关注 h_addr_list 。下面简要说明上述结构体的成员: 53 | 54 | - h_name:该变量中存有官方域名(Official domain name)。官方域名代表某一主页,但实际上,一些著名公司的域名并没有用官方域名注册。 55 | - h_aliases:可以通过多个域名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可以指定其他域名。这些信息可以通过 h_aliases 获得。 56 | - h_addrtype:gethostbyname 函数不仅支持 IPV4 还支持 IPV6 。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPV4 ,则此变量中存有 AF_INET。 57 | - h_length:保存IP地址长度。若是 IPV4 地址,因为是 4 个字节,则保存4;IPV6 时,因为是 16 个字节,故保存 16 58 | - h_addr_list:这个是最重要的的成员。通过此变量以整数形式保存域名相对应的IP地址。另外,用户比较多的网站有可能分配多个IP地址给同一个域名,利用多个服务器做负载均衡,。此时可以通过此变量获取IP地址信息。 59 | 60 | 调用 gethostbyname 函数后,返回的结构体变量如图所示: 61 | 62 | ![](https://i.loli.net/2019/01/18/5c41898ae45e8.png) 63 | 64 | 下面的代码通过一个例子来演示 gethostbyname 的应用,并说明 hostent 结构体变量特性。 65 | 66 | - [gethostbyname.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch08/gethostbyname.c) 67 | 68 | 编译运行: 69 | 70 | ```shell 71 | gcc gethostbyname.c -o hostname 72 | ./hostname www.baidu.com 73 | ``` 74 | 75 | 结果: 76 | 77 | ![](https://i.loli.net/2019/01/18/5c418faf20495.png) 78 | 79 | 如图所示,显示出了对百度的域名解析 80 | 81 | 可以看出,百度有一个域名解析是 CNAME 解析的,指向了`shifen.com`,关于百度具体的解析过程。 82 | 83 | > 这一部分牵扯到了很多关于DNS解析的过程,还有 Linux 下关于域名解析的一些命令,我找了一部分资料,可以点下面的链接查看比较详细的: 84 | > 85 | > - [关于百度DNS的解析过程](http://zhan.renren.com/starshen?gid=3602888498023142484&checked=true) 86 | > - [DNS解析的过程是什么,求详细的?](https://www.zhihu.com/question/23042131/answer/66571369) 87 | > - [Linux DNS 查询剖析](https://zhuanlan.zhihu.com/p/45535596) 88 | > - [Linux DNS查询命令](http://www.live-in.org/archives/1938.html) 89 | > - [Linux中DNS服务器地址查询命令nslookup使用教程](https://blog.csdn.net/shangdi1988/article/details/65713077) 90 | > - [DNS 原理入门](http://www.ruanyifeng.com/blog/2016/06/dns.html) 91 | > 92 | 93 | 仔细阅读这一段代码: 94 | 95 | ```c 96 | inet_ntoa(*(struct in_addr *)host->h_addr_list[i]) 97 | ``` 98 | 99 | 若只看 hostent 的定义,结构体成员 h_addr_list 指向字符串指针数组(由多个字符串地址构成的数组)。但是字符串指针数组保存的元素实际指向的是 in_addr 结构体变量中地址值而非字符串,也就是说`(struct in_addr *)host->h_addr_list[i]`其实是一个指针,然后用`*`符号取具体的值。如图所示: 100 | 101 | ![](https://i.loli.net/2019/01/18/5c419658a73b8.png) 102 | 103 | #### 8.2.3 利用IP地址获取域名 104 | 105 | 请看下面的函数定义: 106 | 107 | ```c 108 | #include 109 | struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family); 110 | /* 111 | 成功时返回 hostent 结构体变量地址值,失败时返回 NULL 指针 112 | addr: 含有IP地址信息的 in_addr 结构体指针。为了同时传递 IPV4 地址之外的全部信息,该变量的类型声明为 char 指针 113 | len: 向第一个参数传递的地址信息的字节数,IPV4时为 4 ,IPV6 时为16. 114 | family: 传递地址族信息,ipv4 是 AF_INET ,IPV6是 AF_INET6 115 | */ 116 | ``` 117 | 118 | 下面的代码演示使用方法: 119 | 120 | - [gethostbyaddr.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch08/gethostbyaddr.c) 121 | 122 | 编译运行: 123 | 124 | ```shell 125 | gcc gethostbyaddr.c -o hostaddr 126 | ./hostaddr 8.8.8.8 127 | ``` 128 | 129 | 结果: 130 | 131 | ![](https://i.loli.net/2019/01/18/5c41a019085d4.png) 132 | 133 | 从图上可以看出,`8.8.8.8`这个IP地址是谷歌的。 134 | 135 | ### 8.3 基于 Windows 的实现 136 | 137 | 暂略 138 | 139 | ### 8.4 习题 140 | 141 | > 以下答案仅代表本人个人观点,可能不是正确答案。 142 | 143 | 1. **下列关于DNS的说法错误的是?** 144 | 145 | 答:字体加粗的表示正确答案。 146 | 147 | 1. **因为DNS从存在,故可以使用域名代替IP** 148 | 2. DNS服务器实际上是路由器,因为路由器根据域名决定数据的路径 149 | 3. **所有域名信息并非集中与 1 台 DNS 服务器,但可以获取某一 DNS 服务器中未注册的所有地址** 150 | 4. DNS 服务器根据操作系统进行区分,Windows 下的 DNS 服务器和 Linux 下的 DNS 服务器是不同的。 151 | 152 | 2. **阅读如下对话,并说明东秀的方案是否可行。(因为对话的字太多,用图代替)** 153 | 154 | ![](https://i.loli.net/2019/01/18/5c41a22f35390.png) 155 | 156 | 答:答案就是可行,DNS 服务器是分布式的,一台坏了可以找其他的。 157 | 158 | 3. **在浏览器地址输入 www.orentec.co.kr ,并整理出主页显示过程。假设浏览器访问默认 DNS 服务器中并没有关于 www.orentec.co.kr 的地址信息.** 159 | 160 | 答:可以参考一下知乎回答,[在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?](https://www.zhihu.com/question/34873227/answer/518086565),我用我自己的理解,简单说一下,首先会去向上一级的 DNS 服务器去查询,通过这种方式逐级向上传递信息,一直到达根服务器时,它知道应该向哪个 DNS 服务器发起询问。向下传递解析请求,得到IP地址候原路返回,最后会将解析的IP地址传递到发起请求的主机。 161 | -------------------------------------------------------------------------------- /ch08/gethostbyaddr.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int i; 12 | struct hostent *host; 13 | struct sockaddr_in addr; 14 | if (argc != 2) 15 | { 16 | printf("Usage : %s \n", argv[0]); 17 | exit(1); 18 | } 19 | 20 | memset(&addr, 0, sizeof(addr)); 21 | addr.sin_addr.s_addr = inet_addr(argv[1]); 22 | host = gethostbyaddr((char *)&addr.sin_addr, 4, AF_INET); 23 | if (!host) 24 | error_handling("gethost... error"); 25 | 26 | printf("Official name: %s \n", host->h_name); 27 | for (i = 0; host->h_aliases[i]; i++) 28 | printf("Aliases %d:%s \n", i + 1, host->h_aliases[i]); 29 | printf("Address type: %s \n", 30 | (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6"); 31 | 32 | for (i = 0; host->h_addr_list[i]; i++) 33 | printf("IP addr %d: %s \n", i + 1, 34 | inet_ntoa(*(struct in_addr *)host->h_addr_list[i])); 35 | 36 | return 0; 37 | } 38 | void error_handling(char *message) 39 | { 40 | fputs(message, stderr); 41 | fputc('\n', stderr); 42 | exit(1); 43 | } -------------------------------------------------------------------------------- /ch08/gethostbyname.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | void error_handling(char *message); 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | int i; 11 | struct hostent *host; 12 | if (argc != 2) 13 | { 14 | printf("Usage : %s \n", argv[0]); 15 | exit(1); 16 | } 17 | // 把参数传递给函数,返回结构体 18 | host = gethostbyname(argv[1]); 19 | if (!host) 20 | error_handling("gethost... error"); 21 | // 输出官方域名 22 | printf("Official name: %s \n", host->h_name); 23 | // Aliases 貌似是解析的 cname 域名? 24 | for (i = 0; host->h_aliases[i]; i++) 25 | printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]); 26 | //看看是不是ipv4 27 | printf("Address type: %s \n", 28 | (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6"); 29 | // 输出ip地址信息 30 | for (i = 0; host->h_addr_list[i]; i++) 31 | printf("IP addr %d: %s \n", i + 1, 32 | inet_ntoa(*(struct in_addr *)host->h_addr_list[i])); 33 | return 0; 34 | } 35 | void error_handling(char *message) 36 | { 37 | fputs(message, stderr); 38 | fputc('\n', stderr); 39 | exit(1); 40 | } -------------------------------------------------------------------------------- /ch09/get_buf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | void error_handling(char *message); 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | int sock; 10 | int snd_buf, rcv_buf, state; 11 | socklen_t len; 12 | 13 | sock = socket(PF_INET, SOCK_STREAM, 0); 14 | len = sizeof(snd_buf); 15 | state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len); 16 | if (state) 17 | error_handling("getsockopt() error"); 18 | 19 | len = sizeof(rcv_buf); 20 | state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len); 21 | if (state) 22 | error_handling("getsockopt() error"); 23 | 24 | printf("Input buffer size: %d \n", rcv_buf); 25 | printf("Output buffer size: %d \n", snd_buf); 26 | 27 | return 0; 28 | } 29 | void error_handling(char *message) 30 | { 31 | fputs(message, stderr); 32 | fputc('\n', stderr); 33 | exit(1); 34 | } -------------------------------------------------------------------------------- /ch09/reuseadr_eserver.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | #define TRUE 1 10 | #define FALSE 0 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int serv_sock, clnt_sock; 15 | char message[30]; 16 | int option, str_len; 17 | socklen_t optlen, clnt_adr_sz; 18 | struct sockaddr_in serv_adr, clnt_adr; 19 | if (argc != 2) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | 25 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 26 | if (serv_sock == -1) 27 | error_handling("socket() error"); 28 | /* 29 | optlen = sizeof(option); 30 | option = TRUE; 31 | setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen); 32 | */ 33 | memset(&serv_adr, 0, sizeof(serv_adr)); 34 | serv_adr.sin_family = AF_INET; 35 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 36 | serv_adr.sin_port = htons(atoi(argv[1])); 37 | 38 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr))) 39 | error_handling("bind() error"); 40 | if (listen(serv_sock, 5) == -1) 41 | error_handling("listen error"); 42 | clnt_adr_sz = sizeof(clnt_adr); 43 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 44 | 45 | while ((str_len = read(clnt_sock, message, sizeof(message))) != 0) 46 | { 47 | write(clnt_sock, message, str_len); 48 | write(1, message, str_len); 49 | } 50 | close(clnt_sock); 51 | close(serv_sock); 52 | return 0; 53 | } 54 | void error_handling(char *message) 55 | { 56 | fputs(message, stderr); 57 | fputc('\n', stderr); 58 | exit(1); 59 | } 60 | -------------------------------------------------------------------------------- /ch09/set_buf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | void error_handling(char *message); 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | int sock; 10 | int snd_buf = 1024 * 3, rcv_buf = 1024 * 3; 11 | int state; 12 | socklen_t len; 13 | 14 | sock = socket(PF_INET, SOCK_STREAM, 0); 15 | len = sizeof(snd_buf); 16 | state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf)); 17 | if (state) 18 | error_handling("setsockopt() error"); 19 | 20 | state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf)); 21 | if (state) 22 | error_handling("setsockopt() error"); 23 | 24 | len = sizeof(snd_buf); 25 | state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len); 26 | if (state) 27 | error_handling("getsockopt() error"); 28 | 29 | len = sizeof(rcv_buf); 30 | state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len); 31 | if (state) 32 | error_handling("getsockopt() error"); 33 | 34 | printf("Input buffer size: %d \n", rcv_buf); 35 | printf("Output buffer size: %d \n", snd_buf); 36 | 37 | return 0; 38 | } 39 | void error_handling(char *message) 40 | { 41 | fputs(message, stderr); 42 | fputc('\n', stderr); 43 | exit(1); 44 | } -------------------------------------------------------------------------------- /ch09/sock_type.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | void error_handling(char *message); 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | int tcp_sock, udp_sock; 10 | int sock_type; 11 | socklen_t optlen; 12 | int state; 13 | 14 | optlen = sizeof(sock_type); 15 | tcp_sock = socket(PF_INET, SOCK_STREAM, 0); 16 | udp_sock = socket(PF_INET, SOCK_DGRAM, 0); 17 | printf("SOCK_STREAM: %d\n", SOCK_STREAM); 18 | printf("SOCK_DGRAM: %d\n", SOCK_DGRAM); 19 | 20 | state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen); 21 | if (state) 22 | error_handling("getsockopt() error"); 23 | printf("Socket type one: %d \n", sock_type); 24 | 25 | state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen); 26 | if (state) 27 | error_handling("getsockopt() error"); 28 | printf("Socket type two: %d \n", sock_type); 29 | return 0; 30 | } 31 | void error_handling(char *message) 32 | { 33 | fputs(message, stderr); 34 | fputc('\n', stderr); 35 | exit(1); 36 | } -------------------------------------------------------------------------------- /ch10/echo_mpclient.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | void read_routine(int sock, char *buf); 11 | void write_routine(int sock, char *buf); 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | int sock; 16 | pid_t pid; 17 | char buf[BUF_SIZE]; 18 | struct sockaddr_in serv_adr; 19 | if (argc != 3) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | sock = socket(PF_INET, SOCK_STREAM, 0); 25 | memset(&serv_adr, 0, sizeof(serv_adr)); 26 | serv_adr.sin_family = AF_INET; 27 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 28 | serv_adr.sin_port = htons(atoi(argv[2])); 29 | 30 | if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 31 | error_handling("connect() error!"); 32 | 33 | pid = fork(); 34 | if (pid == 0) 35 | write_routine(sock, buf); 36 | else 37 | read_routine(sock, buf); 38 | 39 | close(sock); 40 | return 0; 41 | } 42 | 43 | void read_routine(int sock, char *buf) 44 | { 45 | while (1) 46 | { 47 | int str_len = read(sock, buf, BUF_SIZE); 48 | if (str_len == 0) 49 | return; 50 | 51 | buf[str_len] = 0; 52 | printf("Message from server: %s", buf); 53 | } 54 | } 55 | void write_routine(int sock, char *buf) 56 | { 57 | while (1) 58 | { 59 | fgets(buf, BUF_SIZE, stdin); 60 | if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) 61 | { 62 | shutdown(sock, SHUT_WR); //向服务器端传递 EOF,因为fork函数复制了文件描述度,所以通过1次close调用不够 63 | return; 64 | } 65 | write(sock, buf, strlen(buf)); 66 | } 67 | } 68 | 69 | void error_handling(char *message) 70 | { 71 | fputs(message, stderr); 72 | fputc('\n', stderr); 73 | exit(1); 74 | } -------------------------------------------------------------------------------- /ch10/echo_mpserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 30 11 | void error_handling(char *message); 12 | void read_childproc(int sig); 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | int serv_sock, clnt_sock; 17 | struct sockaddr_in serv_adr, clnt_adr; 18 | 19 | pid_t pid; 20 | struct sigaction act; 21 | socklen_t adr_sz; 22 | int str_len, state; 23 | char buf[BUF_SIZE]; 24 | if (argc != 2) 25 | { 26 | printf("Usgae : %s \n", argv[0]); 27 | exit(1); 28 | } 29 | act.sa_handler = read_childproc; //防止僵尸进程 30 | sigemptyset(&act.sa_mask); 31 | act.sa_flags = 0; 32 | state = sigaction(SIGCHLD, &act, 0); //注册信号处理器,把成功的返回值给 state 33 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); //创建服务端套接字 34 | memset(&serv_adr, 0, sizeof(serv_adr)); 35 | serv_adr.sin_family = AF_INET; 36 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 37 | serv_adr.sin_port = htons(atoi(argv[1])); 38 | 39 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) //分配IP地址和端口号 40 | error_handling("bind() error"); 41 | if (listen(serv_sock, 5) == -1) //进入等待连接请求状态 42 | error_handling("listen() error"); 43 | 44 | while (1) 45 | { 46 | adr_sz = sizeof(clnt_adr); 47 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 48 | if (clnt_sock == -1) 49 | continue; 50 | else 51 | puts("new client connected..."); 52 | pid = fork(); //此时,父子进程分别带有一个套接字 53 | if (pid == -1) 54 | { 55 | close(clnt_sock); 56 | continue; 57 | } 58 | if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务 59 | { 60 | close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程 61 | while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0) 62 | write(clnt_sock, buf, str_len); 63 | 64 | close(clnt_sock); 65 | puts("client disconnected..."); 66 | return 0; 67 | } 68 | else 69 | close(clnt_sock); //通过 accept 函数创建的套接字文件描述符已经复制给子进程,因为服务器端要销毁自己拥有的 70 | } 71 | close(serv_sock); 72 | 73 | return 0; 74 | } 75 | 76 | void error_handling(char *message) 77 | { 78 | fputs(message, stderr); 79 | fputc('\n', stderr); 80 | exit(1); 81 | } 82 | void read_childproc(int sig) 83 | { 84 | pid_t pid; 85 | int status; 86 | pid = waitpid(-1, &status, WNOHANG); 87 | printf("removed proc id: %d \n", pid); 88 | } 89 | -------------------------------------------------------------------------------- /ch10/fork.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int gval = 10; 5 | int main(int argc, char *argv[]) 6 | { 7 | pid_t pid; 8 | int lval = 20; 9 | gval++, lval += 5; 10 | 11 | pid = fork(); 12 | if (pid == 0) 13 | gval += 2, lval += 2; 14 | else 15 | gval -= 2, lval -= 2; 16 | 17 | if (pid == 0) 18 | printf("Child Proc: [%d,%d] \n", gval, lval); 19 | else 20 | printf("Parent Proc: [%d,%d] \n", gval, lval); 21 | 22 | return 0; 23 | } 24 | -------------------------------------------------------------------------------- /ch10/homework/kehou3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | pid_t pid; 8 | int sockfd = socket(PF_INET, SOCK_STREAM, 0); 9 | 10 | pid = fork(); 11 | if(pid == 0) 12 | { 13 | printf("Child sockfd: %d \n", sockfd); 14 | } 15 | else 16 | { 17 | printf("Parent sockfd: %d \n", sockfd); 18 | } 19 | return 0; 20 | } 21 | 22 | /* 23 | 结果: 24 | Parent sockfd: 3 25 | Child sockfd: 3 26 | */ -------------------------------------------------------------------------------- /ch10/homework/kehou5.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void ctrl_handling(int sig); 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | struct sigaction act; 10 | act.sa_handler = ctrl_handling; 11 | sigemptyset(&act.sa_mask); 12 | act.sa_flags = 0; 13 | sigaction(SIGINT, &act, 0);//输入ctrl+c发出信号 14 | 15 | while(1) 16 | { 17 | sleep(1); 18 | puts("美好的一天!"); 19 | } 20 | return 0; 21 | } 22 | 23 | void ctrl_handling(int sig) 24 | { 25 | char c; 26 | if(sig == SIGINT) 27 | { 28 | fputs("Do you want to exit(Y to exit)?", stdout); 29 | scanf("%c", &c); 30 | if(c == 'y' || c == 'Y') 31 | exit(1); 32 | } 33 | } -------------------------------------------------------------------------------- /ch10/remove_zomebie.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | void read_childproc(int sig) 8 | { 9 | int status; 10 | pid_t id = waitpid(-1, &status, WNOHANG); 11 | if (WIFEXITED(status)) 12 | { 13 | printf("Removed proc id: %d \n", id); //子进程的 pid 14 | printf("Child send: %d \n", WEXITSTATUS(status)); //子进程的返回值 15 | } 16 | } 17 | 18 | int main(int argc, char *argv[]) 19 | { 20 | pid_t pid; 21 | struct sigaction act; 22 | act.sa_handler = read_childproc; 23 | sigemptyset(&act.sa_mask); 24 | act.sa_flags = 0; 25 | sigaction(SIGCHLD, &act, 0); 26 | 27 | pid = fork(); 28 | if (pid == 0) //子进程执行阶段 29 | { 30 | puts("Hi I'm child process"); 31 | sleep(10); 32 | return 12; 33 | } 34 | else //父进程执行阶段 35 | { 36 | printf("Child proc id: %d\n", pid); 37 | pid = fork(); 38 | if (pid == 0) 39 | { 40 | puts("Hi! I'm child process"); 41 | sleep(10); 42 | exit(24); 43 | } 44 | else 45 | { 46 | int i; 47 | printf("Child proc id: %d \n", pid); 48 | for (i = 0; i < 5; i++) 49 | { 50 | puts("wait"); 51 | sleep(5); 52 | } 53 | } 54 | } 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /ch10/sigaction.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void timeout(int sig) 6 | { 7 | if (sig == SIGALRM) 8 | puts("Time out!"); 9 | alarm(2); 10 | } 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int i; 15 | struct sigaction act; 16 | act.sa_handler = timeout; //保存函数指针 17 | sigemptyset(&act.sa_mask); //将 sa_mask 函数的所有位初始化成0 18 | act.sa_flags = 0; //sa_flags 同样初始化成 0 19 | sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。 20 | 21 | alarm(2); //2 秒后发生 SIGALRM 信号 22 | 23 | for (int i = 0; i < 3; i++) 24 | { 25 | puts("wait..."); 26 | sleep(100); 27 | } 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /ch10/signal.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void timeout(int sig) //信号处理器 6 | { 7 | if (sig == SIGALRM) 8 | puts("Time out!"); 9 | alarm(2); //为了每隔 2 秒重复产生 SIGALRM 信号,在信号处理器中调用 alarm 函数 10 | } 11 | void keycontrol(int sig) //信号处理器 12 | { 13 | if (sig == SIGINT) 14 | puts("CTRL+C pressed"); 15 | } 16 | int main(int argc, char *argv[]) 17 | { 18 | int i; 19 | signal(SIGALRM, timeout); //注册信号及相应处理器 20 | signal(SIGINT, keycontrol); 21 | alarm(2); //预约 2 秒候发生 SIGALRM 信号 22 | 23 | for (i = 0; i < 3; i++) 24 | { 25 | puts("wait..."); 26 | sleep(100); 27 | } 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /ch10/test_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 30 11 | void error_handling(char *message); 12 | void read_childproc(int sig); 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | int serv_sock, clnt_sock; 17 | struct sockaddr_in serv_adr, clnt_adr; 18 | 19 | pid_t pid; 20 | struct sigaction act; 21 | socklen_t adr_sz; 22 | int str_len, state; 23 | char buf[BUF_SIZE]; 24 | if (argc != 2) 25 | { 26 | printf("Usgae : %s \n", argv[0]); 27 | exit(1); 28 | } 29 | act.sa_handler = read_childproc; //防止僵尸进程 30 | sigemptyset(&act.sa_mask); 31 | act.sa_flags = 0; 32 | state = sigaction(SIGCHLD, &act, 0); //注册信号处理器,把成功的返回值给 state 33 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); //创建服务端套接字 34 | 35 | memset(&serv_adr, 0, sizeof(serv_adr)); 36 | serv_adr.sin_family = AF_INET; 37 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 38 | serv_adr.sin_port = htons(atoi(argv[1])); 39 | 40 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) //分配IP地址和端口号 41 | error_handling("bind() error"); 42 | if (listen(serv_sock, 5) == -1) //进入等待连接请求状态 43 | error_handling("listen() error"); 44 | while (1) 45 | { 46 | adr_sz = sizeof(clnt_adr); 47 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 48 | printf("父进程的 serv_sock:%d,clnt_sock:%d\n", serv_sock, clnt_sock); 49 | if (clnt_sock == -1) 50 | continue; 51 | else 52 | puts("new client connected..."); 53 | pid = fork(); //此时,父子进程分别带有一个套接字 54 | if (pid == -1) 55 | { 56 | close(clnt_sock); 57 | continue; 58 | } 59 | if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务 60 | { 61 | printf("子进程的 serv_sock:%d,clnt_sock:%d\n", serv_sock, clnt_sock); 62 | close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程 63 | while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0) 64 | write(clnt_sock, buf, str_len); 65 | 66 | close(clnt_sock); 67 | puts("client disconnected..."); 68 | return 0; 69 | } 70 | else 71 | close(clnt_sock); //通过 accept 函数创建的套接字文件描述符已经复制给子进程,因为服务器端要销毁自己拥有的 72 | } 73 | close(serv_sock); 74 | 75 | return 0; 76 | } 77 | 78 | void error_handling(char *message) 79 | { 80 | fputs(message, stderr); 81 | fputc('\n', stderr); 82 | exit(1); 83 | } 84 | void read_childproc(int sig) 85 | { 86 | pid_t pid; 87 | int status; 88 | pid = waitpid(-1, &status, WNOHANG); 89 | printf("removed proc id: %d \n", pid); 90 | } 91 | -------------------------------------------------------------------------------- /ch10/wait.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | int status; 9 | pid_t pid = fork(); //这里的子进程将在第13行通过 return 语句终止 10 | 11 | if (pid == 0) 12 | { 13 | return 3; 14 | } 15 | else 16 | { 17 | printf("Child PID: %d \n", pid); 18 | pid = fork(); //这里的子进程将在 21 行通过 exit() 函数终止 19 | if (pid == 0) 20 | { 21 | exit(7); 22 | } 23 | else 24 | { 25 | printf("Child PID: %d \n", pid); 26 | wait(&status); //之间终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 27 | if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值 28 | printf("Child send one: %d \n", WEXITSTATUS(status)); 29 | 30 | wait(&status); //因为之前创建了两个进程,所以再次调用 wait 函数和宏 31 | if (WIFEXITED(status)) 32 | printf("Child send two: %d \n", WEXITSTATUS(status)); 33 | sleep(30); 34 | } 35 | } 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /ch10/waitpid.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | int status; 8 | pid_t pid = fork(); 9 | 10 | if (pid == 0) 11 | { 12 | sleep(15); //用 sleep 推迟子进程的执行 13 | return 24; 14 | } 15 | else 16 | { 17 | //调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0 18 | while (!waitpid(-1, &status, WNOHANG)) 19 | { 20 | sleep(3); 21 | puts("sleep 3 sec."); 22 | } 23 | 24 | if (WIFEXITED(status)) 25 | printf("Child send %d \n", WEXITSTATUS(status)); 26 | } 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /ch10/zombie.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | pid_t pid = fork(); 7 | 8 | if (pid == 0) 9 | { 10 | puts("Hi, I am a child Process"); 11 | } 12 | else 13 | { 14 | printf("Child Process ID: %d \n", pid); 15 | sleep(30); 16 | } 17 | 18 | if (pid == 0) 19 | puts("End child proess"); 20 | else 21 | puts("End parent process"); 22 | return 0; 23 | } 24 | -------------------------------------------------------------------------------- /ch11/README.md: -------------------------------------------------------------------------------- 1 | ## 第 11 章 进程间通信 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | 进程间通信,意味着两个不同的进程中可以交换数据 6 | 7 | ### 11.1 进程间通信的基本概念 8 | 9 | #### 11.1.1 通过管道实现进程间通信 10 | 11 | 下图是基于管道(PIPE)的进程间通信的模型: 12 | 13 | ![](https://s2.ax1x.com/2019/01/22/kFlk0s.png) 14 | 15 | 可以看出,为了完成进程间通信,需要创建进程。管道并非属于进程的资源,而是和套接字一样,属于操作系统(也就不是 fork 函数的复制对象)。所以,两个进程通过操作系统提供的内存空间进行通信。下面是创建管道的函数。 16 | 17 | ```c 18 | #include 19 | int pipe(int filedes[2]); 20 | /* 21 | 成功时返回 0 ,失败时返回 -1 22 | filedes[0]: 通过管道接收数据时使用的文件描述符,即管道出口 23 | filedes[1]: 通过管道传输数据时使用的文件描述符,即管道入口 24 | */ 25 | ``` 26 | 27 | 父进程调用函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。但父进程的目的是与子进程进行数据交换,因此需要将入口或出口中的 1 个文件描述符传递给子进程。下面的例子是关于该函数的使用方法: 28 | 29 | - [pipe1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/pipe1.c) 30 | 31 | ```c 32 | #include 33 | #include 34 | #define BUF_SIZE 30 35 | 36 | int main(int argc, char *argv[]) 37 | { 38 | int fds[2]; 39 | char str[] = "Who are you?"; 40 | char buf[BUF_SIZE]; 41 | pid_t pid; 42 | // 调用 pipe 函数创建管道,fds 数组中保存用于 I/O 的文件描述符 43 | pipe(fds); 44 | pid = fork(); //子进程将同时拥有创建管道获取的2个文件描述符,复制的并非管道,而是文件描述符 45 | if (pid == 0) 46 | { 47 | write(fds[1], str, sizeof(str)); 48 | } 49 | else 50 | { 51 | read(fds[0], buf, BUF_SIZE); 52 | puts(buf); 53 | } 54 | return 0; 55 | } 56 | ``` 57 | 58 | 编译运行: 59 | 60 | ```shell 61 | gcc pipe1.c -o pipe1 62 | ./pipe1 63 | ``` 64 | 65 | 结果: 66 | 67 | ``` 68 | Who are you? 69 | ``` 70 | 71 | 可以从程序中看出,首先创建了一个管道,子进程通过 fds[1] 把数据写入管道,父进程从 fds[0] 再把数据读出来。可以从下图看出: 72 | 73 | ![](https://s2.ax1x.com/2019/01/22/kF8A7d.png) 74 | 75 | #### 11.1.2 通过管道进行进程间双向通信 76 | 77 | 下图可以看出双向通信模型: 78 | 79 | ![](https://s2.ax1x.com/2019/01/22/kF84De.png) 80 | 81 | 下面是双向通信的示例: 82 | 83 | - [pipe2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/pipe2.c) 84 | 85 | ```c 86 | #include 87 | #include 88 | #define BUF_SIZE 30 89 | 90 | int main(int argc, char *argv[]) 91 | { 92 | int fds[2]; 93 | char str1[] = "Who are you?"; 94 | char str2[] = "Thank you for your message"; 95 | char buf[BUF_SIZE]; 96 | pid_t pid; 97 | 98 | pipe(fds); 99 | pid = fork(); 100 | if (pid == 0) 101 | { 102 | write(fds[1], str1, sizeof(str1)); 103 | sleep(2); 104 | read(fds[0], buf, BUF_SIZE); 105 | printf("Child proc output: %s \n", buf); 106 | } 107 | else 108 | { 109 | read(fds[0], buf, BUF_SIZE); 110 | printf("Parent proc output: %s \n", buf); 111 | write(fds[1], str2, sizeof(str2)); 112 | sleep(3); 113 | } 114 | return 0; 115 | } 116 | 117 | ``` 118 | 119 | 编译运行: 120 | 121 | ```shell 122 | gcc pipe2.c -o pipe2 123 | ./pipe2 124 | ``` 125 | 126 | 结果: 127 | 128 | ``` 129 | Parent proc output: Who are you? 130 | Child proc output: Thank you for your message 131 | ``` 132 | 133 | 运行结果是正确的,但是如果注释掉第18行的代码,就会出现问题,导致一直等待下去。因为数据进入管道后变成了无主数据。也就是通过 read 函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。因为,注释第18行会产生问题。第19行,自己成将读回自己在第 17 行向管道发送的数据。结果父进程调用 read 函数后,无限期等待数据进入管道。 134 | 135 | 当一个管道不满足需求时,就需要创建两个管道,各自负责不同的数据流动,过程如下图所示: 136 | 137 | ![](https://s2.ax1x.com/2019/01/22/kFJW0e.png) 138 | 139 | 下面采用上述模型改进 `pipe2.c` 。 140 | 141 | - [pipe3.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/pipe3.c) 142 | 143 | ```c 144 | #include 145 | #include 146 | #define BUF_SIZE 30 147 | 148 | int main(int argc, char *argv[]) 149 | { 150 | int fds1[2], fds2[2]; 151 | char str1[] = "Who are you?"; 152 | char str2[] = "Thank you for your message"; 153 | char buf[BUF_SIZE]; 154 | pid_t pid; 155 | 156 | pipe(fds1), pipe(fds2); 157 | pid = fork(); 158 | if (pid == 0) 159 | { 160 | write(fds1[1], str1, sizeof(str1)); 161 | read(fds2[0], buf, BUF_SIZE); 162 | printf("Child proc output: %s \n", buf); 163 | } 164 | else 165 | { 166 | read(fds1[0], buf, BUF_SIZE); 167 | printf("Parent proc output: %s \n", buf); 168 | write(fds2[1], str2, sizeof(str2)); 169 | } 170 | return 0; 171 | } 172 | ``` 173 | 174 | 上面通过创建两个管道实现了功能,此时,不需要额外再使用 sleep 函数。运行结果和上面一样。 175 | 176 | ### 11.2 运用进程间通信 177 | 178 | #### 11.2.1 保存消息的回声服务器 179 | 180 | 下面对第 10 章的 [echo_mpserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpserv.c) 进行改进,添加一个功能: 181 | 182 | > 将回声客户端传输的字符串按序保存到文件中 183 | 184 | 实现该任务将创建一个新进程,从向客户端提供服务的进程读取字符串信息,下面是代码: 185 | 186 | - [echo_storeserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/echo_storeserv.c) 187 | 188 | 编译运行: 189 | 190 | ```shell 191 | gcc echo_storeserv.c -o serv 192 | ./serv 9190 193 | ``` 194 | 195 | 此服务端配合第 10 章的客户端 [echo_mpclient.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpclient.c) 使用,运行结果如下图: 196 | 197 | ![](https://s2.ax1x.com/2019/01/22/kFUCct.png) 198 | 199 | ![](https://s2.ax1x.com/2019/01/22/kFUAHS.png) 200 | 201 | 从图上可以看出,服务端已经生成了文件,把客户端的消息保存可下来,只保存了10次消息。 202 | 203 | ### 11.3 习题 204 | 205 | > 以下答案仅代表本人个人观点,可能不是正确答案。 206 | 207 | 1. **什么是进程间通信?分别从概念和内存的角度进行说明。** 208 | 209 | 答:进程间通信意味着两个不同的进程间可以交换数据。从内存上来说,就是两个进程可以访问同一个内存区域,然后通过这个内存区域数据的变化来进行通信。 210 | 211 | 2. **进程间通信需要特殊的 IPC 机制,这是由于操作系统提供的。进程间通信时为何需要操作系统的帮助?** 212 | 213 | 答:为了进行进程间通信,需要管道的帮助,但是管道不是进程的资源,它属于从操作系统,所以,两个进程通过操作系统提供的内存空间进行通信。 214 | 215 | 3. **「管道」是典型的 IPC 技法。关于管道,请回答以下问题:** 216 | 217 | 1. **管道是进程间交换数据的路径。如何创建此路径?由谁创建?** 218 | 219 | 答:使用 pipe 函数进行创建,由操作系统创建。父进程调用该函数时将创建管道。 220 | 221 | 2. **为了完成进程间通信。2 个进程要同时连接管道。那2 个进程如何连接到同一管道?** 222 | 223 | 答:数组中有两个文件描述符,父子进程调用相关函数时,通过 fork 函数,把 1 个文件描述符传递给子进程。 224 | 225 | 3. **管道允许 2 个进程间的双向通信。双向通信中需要注意哪些内容?** 226 | 227 | 答:向管道传输数据时,先读的进程会把数据取走。简言之,就是数据进入管道候会变成无主数据,所以有时候为了防止错误,需要多个管道来进程通信。 228 | -------------------------------------------------------------------------------- /ch11/echo_storeserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 30 11 | void error_handling(char *message); 12 | void read_childproc(int sig); 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | int serv_sock, clnt_sock; 17 | struct sockaddr_in serv_adr, clnt_adr; 18 | int fds[2]; 19 | 20 | pid_t pid; 21 | struct sigaction act; 22 | socklen_t adr_sz; 23 | int str_len, state; 24 | char buf[BUF_SIZE]; 25 | if (argc != 2) 26 | { 27 | printf("Usgae : %s \n", argv[0]); 28 | exit(1); 29 | } 30 | act.sa_handler = read_childproc; //防止僵尸进程 31 | sigemptyset(&act.sa_mask); 32 | act.sa_flags = 0; 33 | state = sigaction(SIGCHLD, &act, 0); //注册信号处理器,把成功的返回值给 state 34 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); //创建服务端套接字 35 | memset(&serv_adr, 0, sizeof(serv_adr)); 36 | serv_adr.sin_family = AF_INET; 37 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 38 | serv_adr.sin_port = htons(atoi(argv[1])); 39 | 40 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) //分配IP地址和端口号 41 | error_handling("bind() error"); 42 | if (listen(serv_sock, 5) == -1) //进入等待连接请求状态 43 | error_handling("listen() error"); 44 | 45 | pipe(fds); 46 | pid = fork(); 47 | if (pid == 0) 48 | { 49 | FILE *fp = fopen("echomsg.txt", "wt"); 50 | char msgbuf[BUF_SIZE]; 51 | int i, len; 52 | for (int i = 0; i < 10; i++) 53 | { 54 | len = read(fds[0], msgbuf, BUF_SIZE); 55 | fwrite((void *)msgbuf, 1, len, fp); 56 | } 57 | fclose(fp); 58 | return 0; 59 | } 60 | while (1) 61 | { 62 | adr_sz = sizeof(clnt_adr); 63 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 64 | if (clnt_sock == -1) 65 | continue; 66 | else 67 | puts("new client connected..."); 68 | pid = fork(); //此时,父子进程分别带有一个套接字 69 | if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务 70 | { 71 | close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程 72 | while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0) 73 | { 74 | write(clnt_sock, buf, str_len); 75 | write(fds[1], buf, str_len); 76 | } 77 | 78 | close(clnt_sock); 79 | puts("client disconnected..."); 80 | return 0; 81 | } 82 | else 83 | close(clnt_sock); //通过 accept 函数创建的套接字文件描述符已经复制给子进程,因为服务器端要销毁自己拥有的 84 | } 85 | close(serv_sock); 86 | 87 | return 0; 88 | } 89 | 90 | void error_handling(char *message) 91 | { 92 | fputs(message, stderr); 93 | fputc('\n', stderr); 94 | exit(1); 95 | } 96 | void read_childproc(int sig) 97 | { 98 | pid_t pid; 99 | int status; 100 | pid = waitpid(-1, &status, WNOHANG); 101 | printf("removed proc id: %d \n", pid); 102 | } 103 | -------------------------------------------------------------------------------- /ch11/homework/kehou4.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #define BUF_SIZE 30 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | int fds1[2], fds2[2]; 9 | //const char* 以"\0"作为结束符 10 | char str1[] = "Do you like cooffee?"; 11 | char str2[] = "I like coffee"; 12 | char str3[] = "I like long legs"; 13 | char * str_arr[] = {str1, str2, str3}; 14 | char buf[BUF_SIZE]; 15 | pid_t pid; 16 | int i; 17 | 18 | pipe(fds1), pipe(fds2); 19 | pid = fork(); 20 | 21 | if(pid == 0) 22 | { 23 | for(i = 0; i < 3; ++i) 24 | { 25 | //strlen所作的是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描, 26 | //直到碰到第一个字符串结束符'\0'为止,然后返回计数器值(长度不包含'\0') 27 | write(fds1[1], str_arr[i], strlen(str_arr[i]) + 1);//这里长度必须加上1,将字符串结束符加进去,否则会发生消息错乱 28 | read(fds2[0], buf, BUF_SIZE); 29 | printf("子进程收到的消息:%s\n", buf); 30 | } 31 | } 32 | else 33 | { 34 | for(i = 0; i < 3; ++i) 35 | { 36 | read(fds1[0], buf, BUF_SIZE); 37 | printf("父进程收到的消息:%s\n", buf); 38 | write(fds2[1], str_arr[i], strlen(str_arr[i]) + 1); 39 | } 40 | } 41 | return 0; 42 | } -------------------------------------------------------------------------------- /ch11/pipe1.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define BUF_SIZE 30 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | int fds[2]; 8 | char str[] = "Who are you?"; 9 | char buf[BUF_SIZE]; 10 | pid_t pid; 11 | // 调用 pipe 函数创建管道,fds 数组中保存用于 I/O 的文件描述符 12 | pipe(fds); 13 | pid = fork(); //子进程将同时拥有创建管道获取的2个文件描述符,复制的并非管道,而是文件描述符 14 | if (pid == 0) 15 | { 16 | write(fds[1], str, sizeof(str)); 17 | } 18 | else 19 | { 20 | read(fds[0], buf, BUF_SIZE); 21 | puts(buf); 22 | } 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /ch11/pipe2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define BUF_SIZE 30 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | int fds[2]; 8 | char str1[] = "Who are you?"; 9 | char str2[] = "Thank you for your message"; 10 | char buf[BUF_SIZE]; 11 | pid_t pid; 12 | 13 | pipe(fds); 14 | pid = fork(); 15 | if (pid == 0) 16 | { 17 | write(fds[1], str1, sizeof(str1)); 18 | sleep(2); 19 | read(fds[0], buf, BUF_SIZE); 20 | printf("Child proc output: %s \n", buf); 21 | } 22 | else 23 | { 24 | read(fds[0], buf, BUF_SIZE); 25 | printf("Parent proc output: %s \n", buf); 26 | write(fds[1], str2, sizeof(str2)); 27 | sleep(3); 28 | } 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /ch11/pipe3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define BUF_SIZE 30 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | int fds1[2], fds2[2]; 8 | char str1[] = "Who are you?"; 9 | char str2[] = "Thank you for your message"; 10 | char buf[BUF_SIZE]; 11 | pid_t pid; 12 | 13 | pipe(fds1), pipe(fds2); 14 | pid = fork(); 15 | if (pid == 0) 16 | { 17 | write(fds1[1], str1, sizeof(str1)); 18 | read(fds2[0], buf, BUF_SIZE); 19 | printf("Child proc output: %s \n", buf); 20 | } 21 | else 22 | { 23 | read(fds1[0], buf, BUF_SIZE); 24 | printf("Parent proc output: %s \n", buf); 25 | write(fds2[1], str2, sizeof(str2)); 26 | } 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /ch12/README.md: -------------------------------------------------------------------------------- 1 | ## 第 12 章 I/O 复用 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | ### 12.1 基于 I/O 复用的服务器端 6 | 7 | #### 12.1.1 多进程服务端的缺点和解决方法 8 | 9 | 为了构建并发服务器,只要有客户端连接请求就会创建新进程。这的确是实际操作中采用的一种方案,但并非十全十美,因为创建进程要付出很大的代价。这需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,所以相互间的数据交换也要采用相对复杂的方法(IPC 属于相对复杂的通信方法) 10 | 11 | I/O 复用技术可以解决这个问题。 12 | 13 | #### 12.1.2 理解复用 14 | 15 | 「复用」在电子及通信工程领域很常见,向这些领域的专家询问其概念,可能会得到如下答复: 16 | 17 | > 在 1 个通信频道中传递多个数据(信号)的技术 18 | 19 | 「复用」的含义: 20 | 21 | > 为了提高物理设备的效率,只用最少的物理要素传递最多数据时使用的技术 22 | 23 | 上述两种方法的内容完全一致。可以用纸电话模型做一个类比: 24 | 25 | ![](https://s2.ax1x.com/2019/01/23/kA8H81.png) 26 | 27 | 上图是一个纸杯电话系统,为了使得三人同时通话,说话时要同事对着两个纸杯,接听时也需要耳朵同时对准两个纸杯。为了完成 3 人通话,可以进行如下图的改进: 28 | 29 | ![](https://s2.ax1x.com/2019/01/23/kA8bgx.png) 30 | 31 | 如图做出改进,就是引入了复用技术。 32 | 33 | 复用技术的优点: 34 | 35 | - 减少连线长度 36 | - 减少纸杯个数 37 | 38 | 即使减少了连线和纸杯的量仍然可以进行三人同时说话,但是如果碰到以下情况: 39 | 40 | > 「好像不能同时说话?」 41 | 42 | 实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是**「时分复用」**技术。因为说话人声频率不同,即使在同时说话也能进行一定程度上的区分(杂音也随之增多)。因此,也可以说是「频分复用技术」。 43 | 44 | #### 12.1.3 复用技术在服务器端的应用 45 | 46 | 纸杯电话系统引入复用技术之后可以减少纸杯数量和连线长度。服务器端引入复用技术可以减少所需进程数。下图是多进程服务端的模型: 47 | 48 | ![](https://s2.ax1x.com/2019/01/23/kAGBM6.png) 49 | 50 | 下图是引入复用技术之后的模型: 51 | 52 | ![](https://s2.ax1x.com/2019/01/23/kAGrqO.png) 53 | 54 | 从图上可以看出,引入复用技术之后,可以减少进程数。重要的是,无论连接多少客户端,提供服务的进程只有一个。 55 | 56 | ### 12.2 理解 select 函数并实现服务端 57 | 58 | select 函数是最具代表性的实现复用服务器的方法。在 Windows 平台下也有同名函数,所以具有很好的移植性。 59 | 60 | #### 12.2.1 select 函数的功能和调用顺序 61 | 62 | 使用 select 函数时可以将多个文件描述符集中到一起统一监视,项目如下: 63 | 64 | - 是否存在套接字接收数据? 65 | - 无需阻塞传输数据的套接字有哪些? 66 | - 哪些套接字发生了异常? 67 | 68 | > 术语:「事件」。当发生监视项对应情况时,称「发生了事件」。 69 | 70 | select 函数的使用方法与一般函数的区别并不大,更准确的说,他很难使用。但是为了实现 I/O 复用服务器端,我们应该掌握 select 函数,并运用于套接字编程当中。认为「select 函数是 I/O 复用的全部内容」也并不为过。select 函数的调用过程如下图所示: 71 | 72 | ![](https://s2.ax1x.com/2019/01/23/kAtdRs.png) 73 | 74 | #### 12.2.2 设置文件描述符 75 | 76 | 利用 select 函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中在一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述 3 种监视项分成 3 类。 77 | 78 | 利用 fd_set 数组变量执行此操作,如图所示,该数组是存有0和1的位数组。 79 | 80 | ![](https://s2.ax1x.com/2019/01/23/kAt2i4.png) 81 | 82 | 图中最左端的位表示文件描述符 0(所在位置)。如果该位设置为 1,则表示该文件描述符是监视对象。那么图中哪些文件描述符是监视对象呢?很明显,是描述符 1 和 3。在 fd_set 变量中注册或更改值的操作都由下列宏完成。 83 | 84 | - `FD_ZERO(fd_set *fdset)`:将 fd_set 变量所指的位全部初始化成0 85 | - `FD_SET(int fd,fd_set *fdset)`:在参数 fdset 指向的变量中注册文件描述符 fd 的信息 86 | - `FD_CLR(int fd,fd_set *fdset)`:从参数 fdset 指向的变量中清除文件描述符 fd 的信息 87 | - `FD_ISSET(int fd,fd_set *fdset)`:若参数 fdset 指向的变量中包含文件描述符 fd 的信息,则返回「真」 88 | 89 | 上述函数中,FD_ISSET 用于验证 select 函数的调用结果,通过下图解释这些函数的功能: 90 | 91 | ![](https://s2.ax1x.com/2019/01/23/kANR78.png) 92 | 93 | #### 12.2.3 设置检查(监视)范围及超时 94 | 95 | 下面是 select 函数的定义: 96 | 97 | ```c 98 | #include 99 | #include 100 | 101 | int select(int maxfd, fd_set *readset, fd_set *writeset, 102 | fd_set *exceptset, const struct timeval *timeout); 103 | /* 104 | 成功时返回大于 0 的值,失败时返回 -1 105 | maxfd: 监视对象文件描述符数量 106 | readset: 将所有关注「是否存在待读取数据」的文件描述符注册到 fd_set 型变量,并传递其地址值。 107 | writeset: 将所有关注「是否可传输无阻塞数据」的文件描述符注册到 fd_set 型变量,并传递其地址值。 108 | exceptset: 将所有关注「是否发生异常」的文件描述符注册到 fd_set 型变量,并传递其地址值。 109 | timeout: 调用 select 函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息 110 | 返回值: 发生错误时返回 -1,超时时返回0,。因发生关注的时间返回时,返回大于0的值,该值是发生事件的文件描述符数。 111 | */ 112 | ``` 113 | 114 | 如上所述,select 函数用来验证 3 种监视的变化情况,根据监视项声明 3 个 fd_set 型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数。但在此之前(调用 select 函数之前)需要决定下面两件事: 115 | 116 | 1. 文件描述符的监视(检查)范围是? 117 | 2. 如何设定 select 函数的超时时间? 118 | 119 | 第一,文件描述符的监视范围和 select 的第一个参数有关。实际上,select 函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在 fd_set 变量中的文件描述符数。但每次新建文件描述符时,其值就会增加 1 ,故只需将最大的文件描述符值加 1 再传递给 select 函数即可。加 1 是因为文件描述符的值是从 0 开始的。 120 | 121 | 第二,select 函数的超时时间与 select 函数的最后一个参数有关,其中 timeval 结构体定义如下: 122 | 123 | ```c 124 | struct timeval 125 | { 126 | long tv_sec; 127 | long tv_usec; 128 | }; 129 | ``` 130 | 131 | 本来 select 函数只有在监视文件描述符发生变化时才返回。如果未发生变化,就会进入阻塞状态。指定超时时间就是为了防止这种情况的发生。通过上述结构体变量,将秒数填入 tv_sec 的成员,将微妙数填入 tv_usec 的成员,然后将结构体的地址值传递到 select 函数的最后一个参数。此时,即使文件描述符未发生变化,只要过了指定时间,也可以从函数中返回。不过这种情况下, select 函数返回 0 。因此,可以通过返回值了解原因。如果不想设置超时,则传递 NULL 参数。 132 | 133 | #### 12.2.4 调用 select 函数查看结果 134 | 135 | select 返回正整数时,怎样获知哪些文件描述符发生了变化?向 select 函数的第二到第四个参数传递的 fd_set 变量中将产生如图所示的变化: 136 | 137 | ![](https://s2.ax1x.com/2019/01/23/kA06dx.png) 138 | 139 | 由图可知,select 函数调用完成后,向其传递的 fd_set 变量将发生变化。原来为 1 的所有位将变成 0,但是发生了变化的文件描述符除外。因此,可以认为值仍为 1 的位置上的文件描述符发生了变化。 140 | 141 | #### 12.2.5 select 函数调用示例 142 | 143 | 下面是一个 select 函数的例子: 144 | 145 | - [select.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/select.c) 146 | 147 | 编译运行: 148 | 149 | ```shell 150 | gcc select.c -o select 151 | ./select 152 | ``` 153 | 154 | 结果: 155 | 156 | ![](https://s2.ax1x.com/2019/01/23/kAjgW6.png) 157 | 158 | 可以看出,如果运行后在标准输入流输入数据,就会在标准输出流输出数据,但是如果 5 秒没有输入数据,就提示超时。 159 | 160 | #### 12.2.6 实现 I/O 复用服务器端 161 | 162 | 下面通过 select 函数实现 I/O 复用服务器端。下面是基于 I/O 复用的回声服务器端。 163 | 164 | - [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c) 165 | 166 | 编译运行: 167 | 168 | ```shell 169 | gcc echo_selectserv.c -o selserv 170 | ./selserv 9190 171 | ``` 172 | 173 | 结果: 174 | 175 | ![](https://s2.ax1x.com/2019/01/23/kEkV8H.png) 176 | 177 | 从图上可以看出,虽然只用了一个进程,但是却实现了可以和多个客户端进行通信,这都是利用了 select 的特点。 178 | 179 | ### 12.3 基于 Windows 的实现 180 | 181 | 暂略 182 | 183 | ### 12.4 习题 184 | 185 | > 以下答案仅代表本人个人观点,可能不是正确答案。 186 | 187 | 1. **请解释复用技术的通用含义,并说明何为 I/O 复用。** 188 | 189 | 答:通用含义:在 1 个通信频道中传递多个数据(信号)的技术。IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了。 190 | 191 | 参考文章:[Linux网络编程-IO复用技术](https://www.cnblogs.com/luoxn28/p/6220372.html) 192 | 193 | 2. **多进程并发服务器的缺点有哪些?如何在 I/O 复用服务器中弥补?** 194 | 195 | 答:多进程需要进行大量的运算和大量的内存空间。在 I/O 复用服务器中通过 select 函数监视文件描述符,通过判断变化的文件描述符,来得知变化的套接字是哪个,从而实时应答来自多个客户端的请求。 196 | 197 | 3. **复用服务器端需要 select 函数。下列关于 select 函数使用方法的描述错误的是?** 198 | 199 | 答:以下加粗的为正确的描述。 200 | 201 | 1. 调用 select 函数前需要集中 I/O 监视对象的文件描述符 202 | 2. **若已通过 select 函数注册为监视对象,则后续调用 select 函数时无需重复注册** 203 | 3. 复用服务器端同一时间只能服务于 1 个客户端,因此,需要服务的客户端接入服务器端后只能等待 204 | 4. **与多线程服务端不同,基于 select 的复用服务器只需要 1 个进程。因此,可以减少因创建多进程产生的服务器端的负担**。 205 | 206 | 4. **select 函数的观察对象中应包含服务端套接字(监听套接字),那么应将其包含到哪一类监听对象集合?请说明原因**。 207 | 208 | 答:应该包含到「是否存在待读取数据」,因为服务器端需要查看套接字中有没有可以读取的数据。 209 | -------------------------------------------------------------------------------- /ch12/echo_selectserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 100 11 | void error_handling(char *message); 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | int serv_sock, clnt_sock; 16 | struct sockaddr_in serv_adr, clnt_adr; 17 | struct timeval timeout; 18 | fd_set reads, cpy_reads; 19 | 20 | socklen_t adr_sz; 21 | int fd_max, str_len, fd_num, i; 22 | char buf[BUF_SIZE]; 23 | if (argc != 2) 24 | { 25 | printf("Usage : %s \n", argv[0]); 26 | exit(1); 27 | } 28 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 32 | serv_adr.sin_port = htons(atoi(argv[1])); 33 | 34 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 35 | error_handling("bind() error"); 36 | if (listen(serv_sock, 5) == -1) 37 | error_handling("listen() error"); 38 | 39 | FD_ZERO(&reads); 40 | FD_SET(serv_sock, &reads); //注册服务端套接字 41 | fd_max = serv_sock; 42 | 43 | while (1) 44 | { 45 | cpy_reads = reads; 46 | timeout.tv_sec = 5; 47 | timeout.tv_usec = 5000; 48 | 49 | if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) //开始监视,每次重新监听 50 | break; 51 | if (fd_num == 0) 52 | continue; 53 | 54 | for (i = 0; i < fd_max + 1; i++) 55 | { 56 | if (FD_ISSET(i, &cpy_reads)) //查找发生变化的套接字文件描述符 57 | { 58 | if (i == serv_sock) //如果是服务端套接字时,受理连接请求 59 | { 60 | adr_sz = sizeof(clnt_adr); 61 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 62 | 63 | FD_SET(clnt_sock, &reads); //注册一个clnt_sock 64 | if (fd_max < clnt_sock) 65 | fd_max = clnt_sock; 66 | printf("Connected client: %d \n", clnt_sock); 67 | } 68 | else //不是服务端套接字时 69 | { 70 | str_len = read(i, buf, BUF_SIZE); //i指的是当前发起请求的客户端 71 | if (str_len == 0) 72 | { 73 | FD_CLR(i, &reads); 74 | close(i); 75 | printf("closed client: %d \n", i); 76 | } 77 | else 78 | { 79 | write(i, buf, str_len); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | close(serv_sock); 86 | return 0; 87 | } 88 | 89 | void error_handling(char *message) 90 | { 91 | fputs(message, stderr); 92 | fputc('\n', stderr); 93 | exit(1); 94 | } 95 | -------------------------------------------------------------------------------- /ch12/select.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #define BUF_SIZE 30 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | fd_set reads, temps; 10 | int result, str_len; 11 | char buf[BUF_SIZE]; 12 | struct timeval timeout; 13 | 14 | FD_ZERO(&reads); //初始化变量 15 | FD_SET(0, &reads); //将文件描述符0对应的位设置为1 16 | 17 | /* 18 | timeout.tv_sec=5; 19 | timeout.tv_usec=5000; 20 | */ 21 | 22 | while (1) 23 | { 24 | temps = reads; //为了防止调用了select 函数后,位的内容改变,先提前存一下 25 | timeout.tv_sec = 5; 26 | timeout.tv_usec = 0; 27 | result = select(1, &temps, 0, 0, &timeout); //如果控制台输入数据,则返回大于0的数,没有就会超时 28 | if (result == -1) 29 | { 30 | puts("select error!"); 31 | break; 32 | } 33 | else if (result == 0) 34 | { 35 | puts("Time-out!"); 36 | } 37 | else 38 | { 39 | if (FD_ISSET(0, &temps)) //验证发生变化的值是否是标准输入端 40 | { 41 | str_len = read(0, buf, BUF_SIZE); 42 | buf[str_len] = 0; 43 | printf("message from console: %s", buf); 44 | } 45 | } 46 | } 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /ch13/oob_recv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 30 11 | void error_handling(char *message); 12 | void urg_handler(int signo); 13 | 14 | int acpt_sock; 15 | int recv_sock; 16 | 17 | int main(int argc, char *argv[]) 18 | { 19 | struct sockaddr_in recv_adr, serv_adr; 20 | int str_len, state; 21 | socklen_t serv_adr_sz; 22 | struct sigaction act; 23 | char buf[BUF_SIZE]; 24 | if (argc != 2) 25 | { 26 | printf("Usage : %s \n", argv[0]); 27 | exit(1); 28 | } 29 | act.sa_handler = urg_handler; 30 | sigemptyset(&act.sa_mask); 31 | act.sa_flags = 0; 32 | 33 | acpt_sock = socket(PF_INET, SOCK_STREAM, 0); 34 | memset(&recv_adr, 0, sizeof(recv_adr)); 35 | recv_adr.sin_family = AF_INET; 36 | recv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 37 | recv_adr.sin_port = htons(atoi(argv[1])); 38 | 39 | if (bind(acpt_sock, (struct sockaddr *)&recv_adr, sizeof(recv_adr)) == -1) 40 | error_handling("bind() error"); 41 | listen(acpt_sock, 5); 42 | 43 | serv_adr_sz = sizeof(serv_adr); 44 | recv_sock = accept(acpt_sock, (struct sockaddr *)&serv_adr, &serv_adr_sz); 45 | //将文件描述符 recv_sock 指向的套接字拥有者(F_SETOWN)改为把getpid函数返回值用做id的进程 46 | fcntl(recv_sock, F_SETOWN, getpid()); 47 | state = sigaction(SIGURG, &act, 0); //SIGURG 是一个信号,当接收到 MSG_OOB 紧急消息时,系统产生SIGURG信号 48 | 49 | while ((str_len = recv(recv_sock, buf, sizeof(buf), 0)) != 0) 50 | { 51 | if (str_len == -1) 52 | continue; 53 | buf[str_len] = 0; 54 | puts(buf); 55 | } 56 | close(recv_sock); 57 | close(acpt_sock); 58 | return 0; 59 | } 60 | void urg_handler(int signo) 61 | { 62 | int str_len; 63 | char buf[BUF_SIZE]; 64 | str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB); 65 | buf[str_len] = 0; 66 | printf("Urgent message: %s \n", buf); 67 | } 68 | 69 | void error_handling(char *message) 70 | { 71 | fputs(message, stderr); 72 | fputc('\n', stderr); 73 | exit(1); 74 | } -------------------------------------------------------------------------------- /ch13/oob_send.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | struct sockaddr_in recv_adr; 15 | if (argc != 3) 16 | { 17 | printf("Usage : %s \n", argv[0]); 18 | exit(1); 19 | } 20 | sock = socket(PF_INET, SOCK_STREAM, 0); 21 | memset(&recv_adr, 0, sizeof(recv_adr)); 22 | recv_adr.sin_family = AF_INET; 23 | recv_adr.sin_addr.s_addr = inet_addr(argv[1]); 24 | recv_adr.sin_port = htons(atoi(argv[2])); 25 | 26 | if (connect(sock, (struct sockaddr *)&recv_adr, sizeof(recv_adr)) == -1) 27 | error_handling("connect() error"); 28 | 29 | write(sock, "123", strlen("123")); 30 | send(sock, "4", strlen("4"), MSG_OOB); 31 | write(sock, "567", strlen("567")); 32 | send(sock, "890", strlen("890"), MSG_OOB); 33 | close(sock); 34 | return 0; 35 | } 36 | 37 | void error_handling(char *message) 38 | { 39 | fputs(message, stderr); 40 | fputc('\n', stderr); 41 | exit(1); 42 | } -------------------------------------------------------------------------------- /ch13/peek_recv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int acpt_sock, recv_sock; 14 | struct sockaddr_in acpt_adr, recv_adr; 15 | int str_len, state; 16 | socklen_t recv_adr_sz; 17 | char buf[BUF_SIZE]; 18 | if (argc != 2) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | acpt_sock = socket(PF_INET, SOCK_STREAM, 0); 24 | memset(&acpt_adr, 0, sizeof(acpt_adr)); 25 | acpt_adr.sin_family = AF_INET; 26 | acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY); 27 | acpt_adr.sin_port = htons(atoi(argv[1])); 28 | 29 | if (bind(acpt_sock, (struct sockaddr *)&acpt_adr, sizeof(acpt_adr)) == -1) 30 | error_handling("bind() error"); 31 | listen(acpt_sock, 5); 32 | 33 | recv_adr_sz = sizeof(recv_adr); 34 | recv_sock = accept(acpt_sock, (struct sockaddr *)&recv_adr, &recv_adr_sz); 35 | 36 | while (1) 37 | { 38 | //保证就算不存在待读取数据也不会阻塞 39 | str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT); 40 | if (str_len > 0) 41 | break; 42 | } 43 | 44 | buf[str_len] = 0; 45 | printf("Buffering %d bytes : %s \n", str_len, buf); 46 | //再次调用 recv 函数,这一次没有设置任何可选项,所以可以直接从缓冲区读出 47 | str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0); 48 | buf[str_len] = 0; 49 | printf("Read again: %s \n", buf); 50 | close(acpt_sock); 51 | close(recv_sock); 52 | return 0; 53 | } 54 | 55 | void error_handling(char *message) 56 | { 57 | fputs(message, stderr); 58 | fputc('\n', stderr); 59 | exit(1); 60 | } -------------------------------------------------------------------------------- /ch13/peek_send.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | void error_handling(char *message); 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int sock; 12 | struct sockaddr_in send_adr; 13 | if (argc != 3) 14 | { 15 | printf("Usage : %s \n", argv[0]); 16 | exit(1); 17 | } 18 | sock = socket(PF_INET, SOCK_STREAM, 0); 19 | memset(&send_adr, 0, sizeof(send_adr)); 20 | send_adr.sin_family = AF_INET; 21 | send_adr.sin_addr.s_addr = inet_addr(argv[1]); 22 | send_adr.sin_port = htons(atoi(argv[2])); 23 | 24 | if (connect(sock, (struct sockaddr *)&send_adr, sizeof(send_adr)) == -1) 25 | error_handling("connect() error"); 26 | 27 | write(sock, "123", strlen("123")); 28 | close(sock); 29 | return 0; 30 | } 31 | 32 | void error_handling(char *message) 33 | { 34 | fputs(message, stderr); 35 | fputc('\n', stderr); 36 | exit(1); 37 | } -------------------------------------------------------------------------------- /ch13/readv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define BUF_SIZE 100 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | struct iovec vec[2]; 8 | char buf1[BUF_SIZE] = { 9 | 0, 10 | }; 11 | char buf2[BUF_SIZE] = { 12 | 0, 13 | }; 14 | int str_len; 15 | 16 | vec[0].iov_base = buf1; 17 | vec[0].iov_len = 5; 18 | vec[1].iov_base = buf2; 19 | vec[1].iov_len = BUF_SIZE; 20 | 21 | str_len = readv(0, vec, 2); 22 | printf("Read bytes: %d \n", str_len); 23 | printf("First message: %s \n", buf1); 24 | printf("Second message: %s \n", buf2); 25 | return 0; 26 | } 27 | -------------------------------------------------------------------------------- /ch13/writev.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | struct iovec vec[2]; 7 | char buf1[] = "ABCDEFG"; 8 | char buf2[] = "1234567"; 9 | int str_len; 10 | 11 | vec[0].iov_base = buf1; 12 | vec[0].iov_len = 3; 13 | vec[1].iov_base = buf2; 14 | vec[1].iov_len = 4; 15 | 16 | str_len = writev(1, vec, 2); 17 | puts(""); 18 | printf("Write bytes: %d \n", str_len); 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /ch14/README.md: -------------------------------------------------------------------------------- 1 | ## 第 14 章 多播与广播 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | ### 14.1 多播 6 | 7 | 多播(Multicast)方式的数据传输是基于 UDP 完成的。因此 ,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。 8 | 9 | #### 14.1.1 多播的数据传输方式以及流量方面的优点 10 | 11 | 多播的数据传输特点可整理如下: 12 | 13 | - 多播服务器端针对特定多播组,只发送 1 次数据。 14 | - 即使只发送 1 次数据,但该组内的所有客户端都会接收数据 15 | - 多播组数可以在 IP 地址范围内任意增加 16 | 17 | 多播组是 D 类IP地址(224.0.0.0~239.255.255.255),「加入多播组」可以理解为通过程序完成如下声明: 18 | 19 | > 在 D 类IP地址中,我希望接收发往目标 239.234.218.234 的多播数据 20 | 21 | 多播是基于 UDP 完成的,也就是说,多播数据包的格式与 UDP 数据包相同。只是与一般的 UDP 数据包不同。向网络传递 1 个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成。如图所示: 22 | 23 | ![](https://i.loli.net/2019/01/27/5c4d310daa6be.png) 24 | 25 | 若通过 TCP 或 UDP 向 1000 个主机发送文件,则共需要传递 1000 次。但是此时如果用多播网络传输文件,则只需要发送一次。这时由 1000 台主机构成的网络中的路由器负责复制文件并传递到主机。就因为这种特性,多播主要用于「多媒体数据实时传输」。 26 | 27 | 另外,理论上可以完成多播通信,但是不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此,为了在不支持多播的路由器中完成多播通信,也会使用隧道(Tunneling)技术。 28 | 29 | #### 14.1.2 路由(Routing)和 TTL(Time to Live,生存时间),以及加入组的办法 30 | 31 | 为了传递多播数据包,必须设置 TTL 。TTL 是 Time to Live的简写,是决定「数据包传递距离」的主要因素。TTL 用整数表示,并且每经过一个路由器就减一。TTL 变为 0 时,该数据包就无法再被传递,只能销毁。因此,TTL 的值设置过大将影响网络流量。当然,设置过小,也无法传递到目标。 32 | 33 | ![](https://i.loli.net/2019/01/27/5c4d3960001eb.png) 34 | 35 | 接下来是 TTL 的设置方法。TTL 是可以通过第九章的套接字可选项完成的。与设置 TTL 相关的协议层为 IPPROTO_IP ,选项名为 IP_MULTICAST_TTL。因此,可以用如下代码把 TTL 设置为 64 36 | 37 | ```c 38 | int send_sock; 39 | int time_live = 64; 40 | ... 41 | send_sock=socket(PF_INET,SOCK_DGRAM,0); 42 | setsockopt(send_sock,IPPROTO_IP,IP_MULTICAST_TTL,(void*)&time_live,sizeof(time_live); 43 | ... 44 | ``` 45 | 46 | 加入多播组也通过设置套接字可选项来完成。加入多播组相关的协议层为 IPPROTO_IP,选项名为 IP_ADD_MEMBERSHIP 。可通过如下代码加入多播组: 47 | 48 | ```c 49 | int recv_sock; 50 | struct ip_mreq join_adr; 51 | ... 52 | recv_sock=socket(PF_INET,SOCK_DGRAM,0); 53 | ... 54 | join_adr.imr_multiaddr.s_addr="多播组地址信息"; 55 | join_adr.imr_interface.s_addr="加入多播组的主机地址信息"; 56 | setsockopt(recv_sock,IPPROTO_IP,IP_ADD_MEMBERSHIP,(void*)&join_adr,sizeof(join_adr); 57 | ... 58 | ``` 59 | 60 | 下面是 ip_mreq 结构体的定义: 61 | 62 | ```c 63 | struct ip_mreq 64 | { 65 | struct in_addr imr_multiaddr; //写入加入组的IP地址 66 | struct in_addr imr_interface; //加入该组的套接字所属主机的IP地址 67 | }; 68 | ``` 69 | 70 | #### 14.1.3 实现多播 Sender 和 Receiver 71 | 72 | 多播中用「发送者」(以下称为 Sender) 和「接收者」(以下称为 Receiver)替代服务器端和客户端。顾名思义,此处的 Sender 是多播数据的发送主体,Receiver 是需要多播组加入过程的数据接收主体。下面是示例,示例的运行场景如下: 73 | 74 | - Sender : 向 AAA 组广播(Broadcasting)文件中保存的新闻信息 75 | - Receiver : 接收传递到 AAA 组的新闻信息。 76 | 77 | 下面是两个代码: 78 | 79 | - [news_sender.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_sender.c) 80 | - [news_receiver.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_receiver.c) 81 | 82 | 编译运行: 83 | 84 | ``` 85 | gcc news_sender.c -o sender 86 | gcc news_receiver.c -o receiver 87 | ./sender 224.1.1.2 9190 88 | ./receiver 224.1.1.2 9190 89 | ``` 90 | 91 | 结果: 92 | 93 | ![](https://i.loli.net/2019/01/28/5c4e85a9aabcc.png) 94 | 95 | 通过结果可以看出,使用 sender 多播信息,通过 receiver 接收广播,如果延迟运行 receiver 将无法接受之前发送的信息。 96 | 97 | ### 14.2 广播 98 | 99 | 广播(Broadcast)在「一次性向多个主机发送数据」这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接受数据。相反,广播只能向同一网络中的主机传输数据。 100 | 101 | #### 14.2.1 广播的理解和实现方法 102 | 103 | 广播是向同一网络中的所有主机传输数据的方法。与多播相同,广播也是通过 UDP 来完成的。根据传输数据时使用的IP地址形式,广播分为以下两种: 104 | 105 | - 直接广播(Directed Broadcast) 106 | - 本地广播(Local Broadcast) 107 | 108 | 二者在实现上的差别主要在于IP地址。直接广播的IP地址中除了网络地址外,其余主机地址全部设置成 1。例如,希望向网络地址 192.12.34 中的所有主机传输数据时,可以向 192.12.34.255 传输。换言之,可以采取直接广播的方式向特定区域内所有主机传输数据。 109 | 110 | 反之,本地广播中使用的IP地址限定为 255.255.255.255 。例如,192.32.24 网络中的主机向 255.255.255.255 传输数据时,数据将传输到 192.32.24 网络中所有主机。 111 | 112 | **数据通信中使用的IP地址是与 UDP 示例的唯一区别。默认生成的套接字会阻止广播,因此,只需通过如下代码更改默认设置。** 113 | 114 | ```c 115 | int send_sock; 116 | int bcast; 117 | ... 118 | send_sock=socket(PF_INET,SOCK_DGRAM,0); 119 | ... 120 | setsockopt(send_sock,SOL_SOCKET,SO_BROADCAST,(void*)&bcast,sizeof(bcast)); 121 | ... 122 | ``` 123 | 124 | ### 14.2.2 实现广播数据的 Sender 和 Receiver 125 | 126 | 下面是广播数据的 Sender 和 Receiver的代码: 127 | 128 | - [news_sender_brd.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_sender_brd.c) 129 | - [news_receiver_brd.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_receiver_brd.c) 130 | 131 | 编译运行: 132 | 133 | ``` 134 | gcc news_receiver_brd.c -o receiver 135 | gcc news_sender_brd.c -o sender 136 | ./sender 255.255.255.255 9190 137 | ./receiver 9190 138 | ``` 139 | 140 | 结果: 141 | 142 | ![](https://i.loli.net/2019/01/28/5c4e9113368dd.png) 143 | 144 | ### 14.3 基于 Windows 的实现 145 | 146 | 暂略 147 | 148 | ### 14.4 习题 149 | 150 | > 以下答案仅代表本人个人观点,可能不是正确答案。 151 | 152 | 1. **TTL 的含义是什么?请从路由器的角度说明较大的 TTL 值与较小的 TTL 值之间的区别及问题。** 153 | 154 | 答:TTL 是决定「数据包传递距离」的主要因素。TTL 每经过一个路由器就减一。TTL 变为 0 时,数据包就无法再被传递,只能销毁。因此,TTL设置过大会影响网络流量。当然,设置过小无法传递到目标。 155 | 156 | 2. **多播与广播的异同点是什么?请从数据通信的角度进行说明**。 157 | 158 | 答:在「一次性向多个主机发送数据」这一点上与多播类似,但传输的数据范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接受数据。相反,广播只能向同一网络中的主机传输数据。 159 | 160 | 3. **下面关于多播的说法描述错误的是**? 161 | 162 | 答:以下内容加粗的为描述正确 163 | 164 | 1. 多播是用来加入多播组的所有主机传输数据的协议 165 | 2. 主机连接到同一网络才能加入到多播组,也就是说,多播组无法跨越多个网络 166 | 3. **能够加入多播组的主机数并无限制,但只能有 1个主机(Sender)向该组发送数据** 167 | 4. **多播时使用的套接字是 UDP 套接字,因为多播是基于 UDP 进行数据通信的。** 168 | 169 | 4. **多播也对网络流量有利,请比较 TCP 交换方式解释其原因** 170 | 171 | 答:TCP 是必须建立一对一的连接,如果要向1000个主机发送文件,就得传递1000次。但是此时用多播方式传输数据,就只需要发送一次。 172 | 173 | 5. **多播方式的数据通信需要 MBone 虚拟网络。换言之,MBone 是用于多播的网络,但它是虚拟网络。请解释此处的「虚拟网络」** 174 | 175 | 答:可以理解为「通过网络中的特殊协议工作的软件概念上的网络」。也就是说, MBone 并非可以触及的物理网络。他是以物理网络为基础,通过软件方法实现的多播通信必备虚拟网络。 176 | -------------------------------------------------------------------------------- /ch14/news.txt: -------------------------------------------------------------------------------- 1 | 这是一段测试信息! 2 | 1 3 | 2 4 | 3 5 | 4 6 | 5 7 | !!!!!!!!!!!!!!!!!!! 8 | -------------------------------------------------------------------------------- /ch14/news_receiver.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int recv_sock; 14 | int str_len; 15 | char buf[BUF_SIZE]; 16 | struct sockaddr_in adr; 17 | struct ip_mreq join_adr; 18 | if (argc != 3) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | recv_sock = socket(PF_INET, SOCK_DGRAM, 0); 24 | memset(&adr, 0, sizeof(adr)); 25 | adr.sin_family = AF_INET; 26 | adr.sin_addr.s_addr = htonl(INADDR_ANY); 27 | adr.sin_port = htons(atoi(argv[2])); 28 | 29 | if (bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr)) == -1) 30 | error_handling("bind() error"); 31 | //初始化结构体 32 | join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]); //多播组地址 33 | join_adr.imr_interface.s_addr = htonl(INADDR_ANY); //待加入的IP地址 34 | //利用套接字选项 IP_ADD_MEMBERSHIP 加入多播组,完成了接受指定的多播组数据的所有准备 35 | setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&join_adr, sizeof(join_adr)); 36 | 37 | while (1) 38 | { 39 | //通过 recvfrom 函数接受多播数据。如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第5 6参数分贝传入 NULL 0 40 | str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0); 41 | if (str_len < 0) 42 | break; 43 | buf[str_len] = 0; 44 | fputs(buf, stdout); 45 | } 46 | close(recv_sock); 47 | return 0; 48 | } 49 | 50 | void error_handling(char *message) 51 | { 52 | fputs(message, stderr); 53 | fputc('\n', stderr); 54 | exit(1); 55 | } -------------------------------------------------------------------------------- /ch14/news_receiver_brd.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int recv_sock; 14 | int str_len; 15 | char buf[BUF_SIZE]; 16 | struct sockaddr_in adr; 17 | if (argc != 2) 18 | { 19 | printf("Usage : %s \n", argv[0]); 20 | exit(1); 21 | } 22 | recv_sock = socket(PF_INET, SOCK_DGRAM, 0); 23 | memset(&adr, 0, sizeof(adr)); 24 | adr.sin_family = AF_INET; 25 | adr.sin_addr.s_addr = htonl(INADDR_ANY); 26 | adr.sin_port = htons(atoi(argv[1])); 27 | 28 | if (bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr)) == -1) 29 | error_handling("bind() error"); 30 | while (1) 31 | { 32 | //通过 recvfrom 函数接受数据。如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第5 6参数分贝传入 NULL 0 33 | str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0); 34 | if (str_len < 0) 35 | break; 36 | buf[str_len] = 0; 37 | fputs(buf, stdout); 38 | } 39 | close(recv_sock); 40 | return 0; 41 | } 42 | 43 | void error_handling(char *message) 44 | { 45 | fputs(message, stderr); 46 | fputc('\n', stderr); 47 | exit(1); 48 | } -------------------------------------------------------------------------------- /ch14/news_sender.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define TTL 64 9 | #define BUF_SIZE 30 10 | void error_handling(char *message); 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int send_sock; 15 | struct sockaddr_in mul_adr; 16 | int time_live = TTL; 17 | FILE *fp; 18 | char buf[BUF_SIZE]; 19 | if (argc != 3) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | send_sock = socket(PF_INET, SOCK_DGRAM, 0); //创建 UDP 套接字 25 | memset(&mul_adr, 0, sizeof(mul_adr)); 26 | mul_adr.sin_family = AF_INET; 27 | mul_adr.sin_addr.s_addr = inet_addr(argv[1]); //必须将IP地址设置为多播地址 28 | mul_adr.sin_port = htons(atoi(argv[2])); 29 | //指定套接字中 TTL 的信息 30 | setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&time_live, sizeof(time_live)); 31 | if ((fp = fopen("news.txt", "r")) == NULL) 32 | error_handling("fopen() error"); 33 | 34 | while (!feof(fp)) //如果文件没结束就返回0 35 | { 36 | fgets(buf, BUF_SIZE, fp); 37 | sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr *)&mul_adr, sizeof(mul_adr)); 38 | sleep(2); 39 | } 40 | fclose(fp); 41 | close(send_sock); 42 | return 0; 43 | } 44 | 45 | void error_handling(char *message) 46 | { 47 | fputs(message, stderr); 48 | fputc('\n', stderr); 49 | exit(1); 50 | } -------------------------------------------------------------------------------- /ch14/news_sender_brd.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 30 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int send_sock; 14 | struct sockaddr_in broad_adr; 15 | FILE *fp; 16 | char buf[BUF_SIZE]; 17 | int so_brd = 1; 18 | if (argc != 3) 19 | { 20 | printf("Usage : %s \n", argv[0]); 21 | exit(1); 22 | } 23 | send_sock = socket(PF_INET, SOCK_DGRAM, 0); //创建 UDP 套接字 24 | memset(&broad_adr, 0, sizeof(broad_adr)); 25 | broad_adr.sin_family = AF_INET; 26 | broad_adr.sin_addr.s_addr = inet_addr(argv[1]); 27 | broad_adr.sin_port = htons(atoi(argv[2])); 28 | setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void *)&so_brd, sizeof(so_brd)); 29 | if ((fp = fopen("news.txt", "r")) == NULL) 30 | error_handling("fopen() error"); 31 | 32 | while (!feof(fp)) //如果文件没结束就返回0 33 | { 34 | fgets(buf, BUF_SIZE, fp); 35 | sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr *)&broad_adr, sizeof(broad_adr)); 36 | sleep(2); 37 | } 38 | fclose(fp); 39 | close(send_sock); 40 | return 0; 41 | } 42 | 43 | void error_handling(char *message) 44 | { 45 | fputs(message, stderr); 46 | fputc('\n', stderr); 47 | exit(1); 48 | } -------------------------------------------------------------------------------- /ch15/README.md: -------------------------------------------------------------------------------- 1 | ## 第 15 章 套接字和标准I/O 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | ### 15.1 标准 I/O 的优点 6 | 7 | #### 15.1.1 标准 I/O 函数的两个优点 8 | 9 | 除了使用 read 和 write 函数收发数据外,还能使用标准 I/O 函数收发数据。下面是标准 I/O 函数的两个优点: 10 | 11 | - 标准 I/O 函数具有良好的移植性 12 | - 标准 I/O 函数可以利用缓冲提高性能 13 | 14 | 创建套接字时,操作系统会准备 I/O 缓冲。此缓冲在执行 TCP 协议时发挥着非常重要的作用。此时若使用标准 I/O 函数,将得到额外的缓冲支持。如下图: 15 | 16 | ![](https://i.loli.net/2019/01/29/5c500e53ad9aa.png) 17 | 18 | 假设使用 fputs 函数进行传输字符串 「Hello」时,首先将数据传递到标准 I/O 缓冲,然后将数据移动到套接字输出缓冲,最后将字符串发送到对方主机。 19 | 20 | 设置缓冲的主要目的是为了提高性能。从以下两点可以说明性能的提高: 21 | 22 | - 传输的数据量 23 | - 数据向输出缓冲移动的次数。 24 | 25 | 比较 1 个字节的数据发送 10 次的情况和 10 个字节发送 1 次的情况。发送数据时,数据包中含有头信息。头信与数据大小无关,是按照一定的格式填入的。假设头信息占 40 个字节,需要传输的数据量也存在较大区别: 26 | 27 | - 1 个字节 10 次:40*10=400 字节 28 | - 10个字节 1 次:40*1=40 字节。 29 | 30 | #### 15.1.2 标准 I/O 函数和系统函数之间的性能对比 31 | 32 | 下面是利用系统函数的示例: 33 | 34 | - [syscpy.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/syscpy.c) 35 | 36 | 下面是使用标准 I/O 函数复制文件 37 | 38 | - [stdcpy.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/stdcpy.c) 39 | 40 | 对于以上两个代码进行测试,明显基于标准 I/O 函数的代码跑的更快 41 | 42 | #### 15.1.3 标准 I/O 函数的几个缺点 43 | 44 | 标准 I/O 函数存在以下几个缺点: 45 | 46 | - 不容易进行双向通信 47 | - 有时可能频繁调用 fflush 函数 48 | - 需要以 FILE 结构体指针的形式返回文件描述符。 49 | 50 | ### 15.2 使用标准 I/O 函数 51 | 52 | #### 15.2.1 利用 fdopen 函数转换为 FILE 结构体指针 53 | 54 | 函数原型如下: 55 | 56 | ```c 57 | #include 58 | FILE *fdopen(int fildes, const char *mode); 59 | /* 60 | 成功时返回转换的 FILE 结构体指针,失败时返回 NULL 61 | fildes : 需要转换的文件描述符 62 | mode : 将要创建的 FILE 结构体指针的模式信息 63 | */ 64 | ``` 65 | 66 | 以下为示例: 67 | 68 | - [desto.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/desto.c) 69 | 70 | ```c 71 | #include 72 | #include 73 | 74 | int main() 75 | { 76 | FILE *fp; 77 | int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); //创建文件并返回文件描述符 78 | if (fd == -1) 79 | { 80 | fputs("file open error", stdout); 81 | return -1; 82 | } 83 | fp = fdopen(fd, "w"); //返回 写 模式的 FILE 指针 84 | fputs("NetWork C programming \n", fp); 85 | fclose(fp); 86 | return 0; 87 | } 88 | ``` 89 | 90 | 编译运行: 91 | 92 | ``` 93 | gcc desto.c -o desto 94 | ./desto 95 | cat data.dat 96 | ``` 97 | 98 | 运行结果: 99 | 100 | ![](https://i.loli.net/2019/01/29/5c5018ff07b29.png) 101 | 102 | 文件描述符转换为 FILE 指针,并可以通过该指针调用标准 I/O 函数。 103 | 104 | #### 15.2.2 利用 fileno 函数转换为文件描述符 105 | 106 | 函数原型如下: 107 | 108 | ```c 109 | #include 110 | int fileno(FILE *stream); 111 | /* 112 | 成功时返回文件描述符,失败时返回 -1 113 | */ 114 | ``` 115 | 116 | 示例: 117 | 118 | - [todes.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/todes.c) 119 | 120 | ```c 121 | #include 122 | #include 123 | 124 | int main() 125 | { 126 | FILE *fp; 127 | int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); 128 | if (fd == -1) 129 | { 130 | fputs("file open error"); 131 | return -1; 132 | } 133 | 134 | printf("First file descriptor : %d \n", fd); 135 | fp = fdopen(fd, "w"); //转成 file 指针 136 | fputs("TCP/IP SOCKET PROGRAMMING \n", fp); 137 | printf("Second file descriptor: %d \n", fileno(fp)); //转回文件描述符 138 | fclose(fp); 139 | return 0; 140 | } 141 | ``` 142 | 143 | ### 15.3 基于套接字的标准 I/O 函数使用 144 | 145 | 把第四章的回声客户端和回声服务端的内容改为基于标准 I/O 函数的数据交换形式。 146 | 147 | 代码如下: 148 | 149 | - [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/echo_client.c) 150 | - [echo_stdserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/echo_stdserv.c) 151 | 152 | 编译运行: 153 | 154 | ```shell 155 | gcc echo_client.c -o eclient 156 | gcc echo_stdserv.c -o eserver 157 | ``` 158 | 159 | 结果: 160 | 161 | ![](https://i.loli.net/2019/01/29/5c502001581bc.png) 162 | 163 | 可以看出,运行结果和第四章相同,这是利用标准 I/O 实现的。 164 | 165 | ### 15.4 习题 166 | 167 | > 以下答案仅代表本人个人观点,可能不是正确答案。 168 | 169 | 1. **请说明标准 I/O 的 2 个优点。他为何拥有这 2 个优点?** 170 | 171 | 答:①具有很高的移植性②有良好的缓冲提高性能。因为这些函数是由 ANSI C 标准定义的。适合所有编程领域。 172 | 173 | 2. **利用标准 I/O 函数传输数据时,下面的说法是错误的**: 174 | 175 | > 调用 fputs 函数传输数据时,调用后应立即开始发送! 176 | 177 | **为何上述说法是错误的?为达到这种效果应该添加哪些处理过程?** 178 | 179 | 答:只是传输到了缓冲中,应该利用 fflush 来刷新缓冲区。 180 | -------------------------------------------------------------------------------- /ch15/desto.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() 5 | { 6 | FILE *fp; 7 | int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); //创建文件并返回文件描述符 8 | if (fd == -1) 9 | { 10 | fputs("file open error", stdout); 11 | return -1; 12 | } 13 | fp = fdopen(fd, "w"); //返回 写 模式的 FILE 指针 14 | fputs("NetWork C programming \n", fp); 15 | fclose(fp); 16 | return 0; 17 | } -------------------------------------------------------------------------------- /ch15/echo_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 1024 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int sock; 14 | char message[BUF_SIZE]; 15 | int str_len; 16 | struct sockaddr_in serv_adr; 17 | FILE *readfp; 18 | FILE *writefp; 19 | if (argc != 3) 20 | { 21 | printf("Usage : %s \n", argv[0]); 22 | exit(1); 23 | } 24 | 25 | sock = socket(PF_INET, SOCK_STREAM, 0); 26 | if (sock == -1) 27 | error_handling("socket() error"); 28 | 29 | memset(&serv_adr, 0, sizeof(serv_adr)); 30 | serv_adr.sin_family = AF_INET; 31 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]); 32 | serv_adr.sin_port = htons(atoi(argv[2])); 33 | 34 | if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 35 | error_handling("connect() error!"); 36 | else 37 | puts("Connected..........."); 38 | readfp = fdopen(sock, "r"); 39 | writefp = fdopen(sock, "w"); 40 | while (1) 41 | { 42 | fputs("Input message(Q to quit): ", stdout); 43 | fgets(message, BUF_SIZE, stdin); 44 | 45 | if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 46 | break; 47 | 48 | fputs(message, writefp); 49 | fflush(writefp); 50 | fgets(message, BUF_SIZE, readfp); 51 | printf("Message from server: %s", message); 52 | } 53 | fclose(writefp); 54 | fclose(readfp); 55 | return 0; 56 | } 57 | 58 | void error_handling(char *message) 59 | { 60 | fputs(message, stderr); 61 | fputc('\n', stderr); 62 | exit(1); 63 | } -------------------------------------------------------------------------------- /ch15/echo_stdserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define BUF_SIZE 1024 9 | void error_handling(char *message); 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | int serv_sock, clnt_sock; 14 | char message[BUF_SIZE]; 15 | int str_len, i; 16 | 17 | struct sockaddr_in serv_adr, clnt_adr; 18 | socklen_t clnt_adr_sz; 19 | FILE *readfp; 20 | FILE *writefp; 21 | 22 | if (argc != 2) 23 | { 24 | printf("Usage : %s \n", argv[0]); 25 | exit(1); 26 | } 27 | 28 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 29 | if (serv_sock == -1) 30 | error_handling("socket() error"); 31 | 32 | memset(&serv_adr, 0, sizeof(serv_adr)); 33 | serv_adr.sin_family = AF_INET; 34 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 35 | serv_adr.sin_port = htons(atoi(argv[1])); 36 | 37 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 38 | error_handling("bind() error"); 39 | 40 | if (listen(serv_sock, 5) == -1) 41 | error_handling("listen() error"); 42 | 43 | clnt_adr_sz = sizeof(clnt_adr); 44 | //调用 5 次 accept 函数,共为 5 个客户端提供服务 45 | for (i = 0; i < 5; i++) 46 | { 47 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 48 | if (clnt_sock == -1) 49 | error_handling("accept() error"); 50 | else 51 | printf("Connect client %d \n", i + 1); 52 | 53 | readfp = fdopen(clnt_sock, "r"); 54 | writefp = fdopen(clnt_sock, "w"); 55 | while (!feof(readfp)) 56 | { 57 | fgets(message, BUF_SIZE, readfp); 58 | fputs(message, writefp); 59 | fflush(writefp); 60 | } 61 | 62 | fclose(readfp); 63 | fclose(writefp); 64 | } 65 | close(serv_sock); 66 | return 0; 67 | } 68 | 69 | void error_handling(char *message) 70 | { 71 | fputs(message, stderr); 72 | fputc('\n', stderr); 73 | exit(1); 74 | } -------------------------------------------------------------------------------- /ch15/stdcpy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #define BUF_SZIE 3 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | FILE *fp1; 7 | FILE *fp2; 8 | char buf[BUF_SZIE]; 9 | 10 | fp1 = fopen("news.txt", "r"); 11 | fp2 = fopen("cpy.txt", "w"); 12 | 13 | while (fgets(buf, BUF_SZIE, fp1) != NULL) 14 | fputs(buf, fp2); 15 | fclose(fp1); 16 | fclose(fp2); 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /ch15/syscpy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define BUF_SIZE 3 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | int fd1, fd2; 8 | int len; 9 | char buf[BUF_SIZE]; 10 | 11 | fd1 = open("news.txt", O_RDONLY); 12 | fd2 = open("cpy.txt", O_WRONLY | O_CREAT | O_TRUNC); 13 | 14 | while ((len = read(fd1, buf, sizeof(buf))) > 0) 15 | write(fd2, buf, len); 16 | 17 | close(fd1); 18 | close(fd2); 19 | 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /ch15/todes.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() 5 | { 6 | FILE *fp; 7 | int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); 8 | if (fd == -1) 9 | { 10 | fputs("file open error",stdout); 11 | return -1; 12 | } 13 | 14 | printf("First file descriptor : %d \n", fd); 15 | fp = fdopen(fd, "w"); //转成 file 指针 16 | fputs("TCP/IP SOCKET PROGRAMMING \n", fp); 17 | printf("Second file descriptor: %d \n", fileno(fp)); //转回文件描述符 18 | fclose(fp); 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /ch16/README.md: -------------------------------------------------------------------------------- 1 | ## 第 16 章 关于 I/O 流分离的其他内容 2 | 3 | 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 4 | 5 | ### 16.1 分离 I/O 流 6 | 7 | 「分离 I/O 流」是一种常用表达。有 I/O 工具可区分二者,无论采用哪种方法,都可以认为是分离了 I/O 流。 8 | 9 | #### 16.1.1 2次 I/O 流分离 10 | 11 | 之前有两种分离方法: 12 | 13 | - 第一种是第 10 章的「TCP I/O 过程」分离。通过调用 fork 函数复制出一个文件描述符,以区分输入和输出中使用的文件描述符。虽然文件描述符本身不会根据输入和输出进行区分,但我们分开了 2 个文件描述符的用途,因此,这也属于「流」的分离。 14 | - 第二种分离是在第 15 章。通过 2 次 fdopen 函数的调用,创建读模式 FILE 指针(FILE 结构体指针)和写模式 FILE 指针。换言之,我们分离了输入工具和输出工具,因此也可视为「流」的分离。下面是分离的理由。 15 | 16 | #### 16.1.2 分离「流」的好处 17 | 18 | 首先是第 10 章「流」的分离目的: 19 | 20 | - 通过分开输入过程(代码)和输出过程降低实现难度 21 | - 与输入无关的输出操作可以提高速度 22 | 23 | 下面是第 15 章「流」分离的目的: 24 | 25 | - 为了将 FILE 指针按读模式和写模式加以区分 26 | - 可以通过区分读写模式降低实现难度 27 | - 通过区分 I/O 缓冲提高缓冲性能 28 | 29 | #### 16.1.3 「流」分离带来的 EOF 问题 30 | 31 | 第 7 章介绍过 EOF 的传递方法和半关闭的必要性。有一个语句: 32 | 33 | ```c 34 | shutdown(sock,SHUT_WR); 35 | ``` 36 | 37 | 当时说过调用 shutdown 函数的基于半关闭的 EOF 传递方法。第十章的 [echo_mpclient.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpclient.c) 添加了半关闭的相关代码。但是还没有讲采用 fdopen 函数怎么半关闭。那么是否是通过 fclose 函数关闭流呢?我们先试试 38 | 39 | 下面是服务端和客户端码: 40 | 41 | - [sep_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_clnt.c) 42 | - [sep_serv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_serv.c) 43 | 44 | 编译运行: 45 | 46 | ```shell 47 | gcc sep_clnt.c -o clnt 48 | gcc sep_serv.c -o serv 49 | ./serv 9190 50 | ./clnt 127.0.0.1 9190 51 | ``` 52 | 53 | 结果: 54 | 55 | ![](https://i.loli.net/2019/01/30/5c512086a75d9.png) 56 | 57 | 从运行结果可以看出,服务端最终没有收到客户端发送的信息。那么这是什么原因呢? 58 | 59 | 原因是:服务端代码的 `fclose(writefp);` 这一句,完全关闭了套接字而不是半关闭。这才是这一章需要解决的问题。 60 | 61 | ### 16.2 文件描述符的的复制和半关闭 62 | 63 | #### 16.2.1 终止「流」时无法半关闭原因 64 | 65 | 下面的图描述的是服务端代码中的两个FILE 指针、文件描述符和套接字中的关系。 66 | 67 | ![](https://i.loli.net/2019/01/30/5c5121da89955.png) 68 | 69 | 从图中可以看到,两个指针都是基于同一文件描述符创建的。因此,针对于任何一个 FILE 指针调用 fclose 函数都会关闭文件描述符,如图所示: 70 | 71 | ![](https://i.loli.net/2019/01/30/5c51224051802.png) 72 | 73 | 从图中看到,销毁套接字时再也无法进行数据交换。那如何进入可以进入但是无法输出的半关闭状态呢?如下图所示: 74 | 75 | ![](https://i.loli.net/2019/01/30/5c5122a45c5f1.png) 76 | 77 | 只需要创建 FILE 指针前先复制文件描述符即可。复制后另外创建一个文件描述符,然后利用各自的文件描述符生成读模式的 FILE 指针和写模式的 FILE 指针。这就为半关闭创造好了环境,因为套接字和文件描述符具有如下关系: 78 | 79 | > 销毁所有文件描述符候才能销毁套接字 80 | 81 | 也就是说,针对写模式 FILE 指针调用 fclose 函数时,只能销毁与该 FILE 指针相关的文件描述符,无法销毁套接字,如下图: 82 | 83 | ![](https://i.loli.net/2019/01/30/5c5123ad7df31.png) 84 | 85 | 那么调用 fclose 函数候还剩下 1 个文件描述符,因此没有销毁套接字。那此时的状态是否为半关闭状态?不是!只是准备好了进入半关闭状态,而不是已经进入了半关闭状态。仔细观察,还剩下一个文件描述符。而该文件描述符可以同时进行 I/O 。因此,不但没有发送 EOF ,而且仍然可以利用文件描述符进行输出。 86 | 87 | #### 16.2.2 复制文件描述符 88 | 89 | 与调用 fork 函数不同,调用 fork 函数将复制整个进程,此处讨论的是同一进程内完成对完成描述符的复制。如图: 90 | 91 | ![](https://i.loli.net/2019/01/30/5c512579c45b6.png) 92 | 93 | 复制完成后,两个文件描述符都可以访问文件,但是编号不同。 94 | 95 | #### 16.2.3 dup 和 dup2 96 | 97 | 下面给出两个函数原型: 98 | 99 | ```c 100 | #include 101 | int dup(int fildes); 102 | int dup2(int fildes, int fildes2); 103 | /* 104 | 成功时返回复制的文件描述符,失败时返回 -1 105 | fildes : 需要复制的文件描述符 106 | fildes2 : 明确指定的文件描述符的整数值。 107 | */ 108 | ``` 109 | 110 | dup2 函数明确指定复制的文件描述符的整数值。向其传递大于 0 且小于进程能生成的最大文件描述符值时,该值将成为复制出的文件描述符值。下面是代码示例: 111 | 112 | - [dup.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/dup.c) 113 | 114 | ```c 115 | #include 116 | #include 117 | 118 | int main(int argc, char *argv[]) 119 | { 120 | int cfd1, cfd2; 121 | char str1[] = "Hi~ \n"; 122 | char str2[] = "It's nice day~ \n"; 123 | 124 | cfd1 = dup(1); //复制文件描述符 1 125 | cfd2 = dup2(cfd1, 7); //再次复制文件描述符,定为数值 7 126 | 127 | printf("fd1=%d , fd2=%d \n", cfd1, cfd2); 128 | write(cfd1, str1, sizeof(str1)); 129 | write(cfd2, str2, sizeof(str2)); 130 | 131 | close(cfd1); 132 | close(cfd2); //终止复制的文件描述符,但是仍有一个文件描述符 133 | write(1, str1, sizeof(str1)); 134 | close(1); 135 | write(1, str2, sizeof(str2)); //无法完成输出 136 | return 0; 137 | } 138 | 139 | ``` 140 | 141 | 编译运行: 142 | 143 | ``` 144 | gcc dup.c -o dup 145 | ./dup 146 | ``` 147 | 148 | 结果: 149 | 150 | ![](https://i.loli.net/2019/01/30/5c5135574d89a.png) 151 | 152 | #### 16.2.4 复制文件描述符后「流」的分离 153 | 154 | 下面更改 [sep_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_clnt.c) 和 [sep_serv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_serv.c) 可以使得让它正常工作,正常工作是指通过服务器的半关闭状态接收客户端最后发送的字符串。 155 | 156 | 下面是代码: 157 | 158 | - [sep_serv2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_serv2.c) 159 | 160 | 这个代码可以与 [sep_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_clnt.c) 配合起来食用,编译过程和上面一样,运行结果为: 161 | 162 | ![](https://i.loli.net/2019/01/30/5c513d54a27e0.png) 163 | 164 | ### 16.3 习题 165 | 166 | > 以下答案仅代表本人个人观点,可能不是正确答案。 167 | 168 | 1. **下列关于 FILE 结构体指针和文件描述符的说法错误的是**? 169 | 170 | 答:以下加粗内容代表说法正确。 171 | 172 | 1. 与 FILE 结构体指针相同,文件描述符也分输入描述符和输出描述符 173 | 2. 复制文件描述符时将生成相同值的描述符,可以通过这 2 个描述符进行 I/O 174 | 3. **可以利用创建套接字时返回的文件描述符进行 I/O ,也可以不通过文件描述符,直接通过 FILE 结构体指针完成** 175 | 4. **可以从文件描述符生成 FILE 结构体指针,而且可以利用这种 FILE 结构体指针进行套接字 I/O** 176 | 5. 若文件描述符为读模式,则基于该描述符生成的 FILE 结构体指针同样是读模式;若文件描述符为写模式,则基于该描述符生成的 FILE 结构体指针同样是写模式 177 | 178 | 2. **EOF 的发送相关描述中错误的是**? 179 | 180 | 答:以下加粗内容代表说法正确。 181 | 182 | 1. 终止文件描述符时发送 EOF 183 | 2. **即使未完成终止文件描述符,关闭输出流时也会发送 EOF** 184 | 3. 如果复制文件描述符,则包括复制的文件描述符在内,所有文件描述符都终止时才会发送 EOF 185 | 4. **即使复制文件描述符,也可以通过调用 shutdown 函数进入半关闭状态并发送 EOF** 186 | -------------------------------------------------------------------------------- /ch16/dup.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | int cfd1, cfd2; 7 | char str1[] = "Hi~ \n"; 8 | char str2[] = "It's nice day~ \n"; 9 | 10 | cfd1 = dup(1); //复制文件描述符 1 11 | cfd2 = dup2(cfd1, 7); //再次复制文件描述符,定为数值 7 12 | 13 | printf("fd1=%d , fd2=%d \n", cfd1, cfd2); 14 | write(cfd1, str1, sizeof(str1)); 15 | write(cfd2, str2, sizeof(str2)); 16 | 17 | close(cfd1); 18 | close(cfd2); //终止复制的文件描述符,但是仍有一个文件描述符 19 | write(1, str1, sizeof(str1)); 20 | close(1); 21 | write(1, str2, sizeof(str2)); //无法完成输出 22 | return 0; 23 | } 24 | -------------------------------------------------------------------------------- /ch16/sep_clnt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #define BUF_SIZE 1024 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int sock; 12 | char buf[BUF_SIZE]; 13 | struct sockaddr_in serv_addr; 14 | 15 | FILE *readfp; 16 | FILE *writefp; 17 | 18 | sock = socket(PF_INET, SOCK_STREAM, 0); 19 | memset(&serv_addr, 0, sizeof(serv_addr)); 20 | serv_addr.sin_family = AF_INET; 21 | serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 22 | serv_addr.sin_port = htons(atoi(argv[2])); 23 | 24 | connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); 25 | readfp = fdopen(sock, "r"); 26 | writefp = fdopen(sock, "w"); 27 | 28 | while (1) 29 | { 30 | if (fgets(buf, sizeof(buf), readfp) == NULL) 31 | break; 32 | fputs(buf, stdout); 33 | fflush(stdout); 34 | } 35 | 36 | fputs("FROM CLIENT: Thank you \n", writefp); 37 | fflush(writefp); 38 | fclose(writefp); 39 | fclose(readfp); 40 | 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /ch16/sep_serv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #define BUF_SIZE 1024 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int serv_sock, clnt_sock; 12 | FILE *readfp; 13 | FILE *writefp; 14 | 15 | struct sockaddr_in serv_adr, clnt_adr; 16 | socklen_t clnt_adr_sz; 17 | char buf[BUF_SIZE] = { 18 | 0, 19 | }; 20 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 21 | memset(&serv_adr, 0, sizeof(serv_adr)); 22 | serv_adr.sin_family = AF_INET; 23 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 24 | serv_adr.sin_port = htons(atoi(argv[1])); 25 | bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 26 | listen(serv_sock, 5); 27 | clnt_adr_sz = sizeof(clnt_adr); 28 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 29 | 30 | readfp = fdopen(clnt_sock, "r"); 31 | writefp = fdopen(clnt_sock, "w"); 32 | 33 | fputs("FROM SERVER: Hi~ client? \n", writefp); 34 | fputs("I love all of the world \n", writefp); 35 | fputs("You are awesome! \n", writefp); 36 | fflush(writefp); 37 | 38 | fclose(writefp); 39 | fgets(buf, sizeof(buf), readfp); 40 | fputs(buf, stdout); 41 | fclose(readfp); 42 | return 0; 43 | } 44 | -------------------------------------------------------------------------------- /ch16/sep_serv2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #define BUF_SIZE 1024 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | int serv_sock, clnt_sock; 12 | FILE *readfp; 13 | FILE *writefp; 14 | 15 | struct sockaddr_in serv_adr, clnt_adr; 16 | socklen_t clnt_adr_sz; 17 | char buf[BUF_SIZE] = { 18 | 0, 19 | }; 20 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 21 | memset(&serv_adr, 0, sizeof(serv_adr)); 22 | serv_adr.sin_family = AF_INET; 23 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 24 | serv_adr.sin_port = htons(atoi(argv[1])); 25 | bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); 26 | listen(serv_sock, 5); 27 | clnt_adr_sz = sizeof(clnt_adr); 28 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 29 | 30 | readfp = fdopen(clnt_sock, "r"); 31 | writefp = fdopen(dup(clnt_sock), "w"); //复制文件描述符 32 | 33 | fputs("FROM SERVER: Hi~ client? \n", writefp); 34 | fputs("I love all of the world \n", writefp); 35 | fputs("You are awesome! \n", writefp); 36 | fflush(writefp); 37 | 38 | shutdown(fileno(writefp), SHUT_WR); //对 fileno 产生的文件描述符使用 shutdown 进入半关闭状态 39 | fclose(writefp); 40 | 41 | fgets(buf, sizeof(buf), readfp); 42 | fputs(buf, stdout); 43 | fclose(readfp); 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /ch17/echo_EDGEserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 2 10 | #define EPOLL_SIZE 50 11 | void error_handling(char *message); 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | int serv_sock, clnt_sock; 16 | struct sockaddr_in serv_adr, clnt_adr; 17 | socklen_t adr_sz; 18 | int str_len, i; 19 | char buf[BUF_SIZE]; 20 | 21 | struct epoll_event *ep_events; 22 | struct epoll_event event; 23 | int epfd, event_cnt; 24 | 25 | if (argc != 2) 26 | { 27 | printf("Usage : %s \n", argv[0]); 28 | exit(1); 29 | } 30 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 31 | memset(&serv_adr, 0, sizeof(serv_adr)); 32 | serv_adr.sin_family = AF_INET; 33 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 34 | serv_adr.sin_port = htons(atoi(argv[1])); 35 | 36 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 37 | error_handling("bind() error"); 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | epfd = epoll_create(EPOLL_SIZE); //可以忽略这个参数,填入的参数为操作系统参考 42 | ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); 43 | 44 | event.events = EPOLLIN; //需要读取数据的情况 45 | event.data.fd = serv_sock; 46 | epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 47 | 48 | while (1) 49 | { 50 | event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //获取改变了的文件描述符,返回数量 51 | if (event_cnt == -1) 52 | { 53 | puts("epoll_wait() error"); 54 | break; 55 | } 56 | 57 | puts("return epoll_wait"); 58 | for (i = 0; i < event_cnt; i++) 59 | { 60 | if (ep_events[i].data.fd == serv_sock) //客户端请求连接时 61 | { 62 | adr_sz = sizeof(clnt_adr); 63 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 64 | event.events = EPOLLIN | EPOLLET; 65 | event.data.fd = clnt_sock; //把客户端套接字添加进去 66 | epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 67 | printf("connected client : %d \n", clnt_sock); 68 | } 69 | else //是客户端套接字时 70 | { 71 | str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 72 | if (str_len == 0) 73 | { 74 | epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //从epoll中删除套接字 75 | close(ep_events[i].data.fd); 76 | printf("closed client : %d \n", ep_events[i].data.fd); 77 | } 78 | else 79 | { 80 | write(ep_events[i].data.fd, buf, str_len); 81 | } 82 | } 83 | } 84 | } 85 | close(serv_sock); 86 | close(epfd); 87 | 88 | return 0; 89 | } 90 | 91 | void error_handling(char *message) 92 | { 93 | fputs(message, stderr); 94 | fputc('\n', stderr); 95 | exit(1); 96 | } 97 | -------------------------------------------------------------------------------- /ch17/echo_EPETserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define BUF_SIZE 4 //缓冲区设置为 4 字节 12 | #define EPOLL_SIZE 50 13 | void setnonblockingmode(int fd); 14 | void error_handling(char *message); 15 | 16 | int main(int argc, char *argv[]) 17 | { 18 | int serv_sock, clnt_sock; 19 | struct sockaddr_in serv_adr, clnt_adr; 20 | socklen_t adr_sz; 21 | int str_len, i; 22 | char buf[BUF_SIZE]; 23 | 24 | struct epoll_event *ep_events; 25 | struct epoll_event event; 26 | int epfd, event_cnt; 27 | 28 | if (argc != 2) 29 | { 30 | printf("Usage : %s \n", argv[0]); 31 | exit(1); 32 | } 33 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 34 | memset(&serv_adr, 0, sizeof(serv_adr)); 35 | serv_adr.sin_family = AF_INET; 36 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 37 | serv_adr.sin_port = htons(atoi(argv[1])); 38 | 39 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 40 | error_handling("bind() error"); 41 | if (listen(serv_sock, 5) == -1) 42 | error_handling("listen() error"); 43 | 44 | epfd = epoll_create(EPOLL_SIZE); //可以忽略这个参数,填入的参数为操作系统参考 45 | ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); 46 | 47 | setnonblockingmode(serv_sock); 48 | event.events = EPOLLIN; //需要读取数据的情况 49 | event.data.fd = serv_sock; 50 | epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 51 | 52 | while (1) 53 | { 54 | event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //获取改变了的文件描述符,返回数量 55 | if (event_cnt == -1) 56 | { 57 | puts("epoll_wait() error"); 58 | break; 59 | } 60 | 61 | puts("return epoll_wait"); 62 | for (i = 0; i < event_cnt; i++) 63 | { 64 | if (ep_events[i].data.fd == serv_sock) //客户端请求连接时 65 | { 66 | adr_sz = sizeof(clnt_adr); 67 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 68 | setnonblockingmode(clnt_sock); //将 accept 创建的套接字改为非阻塞模式 69 | event.events = EPOLLIN | EPOLLET; //改成边缘触发 70 | event.data.fd = clnt_sock; //把客户端套接字添加进去 71 | epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 72 | printf("connected client : %d \n", clnt_sock); 73 | } 74 | else //是客户端套接字时 75 | { 76 | while (1) 77 | { 78 | str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 79 | if (str_len == 0) 80 | { 81 | epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //从epoll中删除套接字 82 | close(ep_events[i].data.fd); 83 | printf("closed client : %d \n", ep_events[i].data.fd); 84 | break; 85 | } 86 | else if (str_len < 0) 87 | { 88 | if (errno == EAGAIN) //read 返回-1 且 errno 值为 EAGAIN ,意味读取了输入缓冲的全部数据 89 | break; 90 | } 91 | else 92 | { 93 | write(ep_events[i].data.fd, buf, str_len); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | close(serv_sock); 100 | close(epfd); 101 | 102 | return 0; 103 | } 104 | 105 | void error_handling(char *message) 106 | { 107 | fputs(message, stderr); 108 | fputc('\n', stderr); 109 | exit(1); 110 | } 111 | void setnonblockingmode(int fd) 112 | { 113 | int flag = fcntl(fd, F_GETFL, 0); 114 | fcntl(fd, F_SETFL, flag | O_NONBLOCK); 115 | } -------------------------------------------------------------------------------- /ch17/echo_EPLTserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 2 10 | #define EPOLL_SIZE 50 11 | void error_handling(char *message); 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | int serv_sock, clnt_sock; 16 | struct sockaddr_in serv_adr, clnt_adr; 17 | socklen_t adr_sz; 18 | int str_len, i; 19 | char buf[BUF_SIZE]; 20 | 21 | struct epoll_event *ep_events; 22 | struct epoll_event event; 23 | int epfd, event_cnt; 24 | 25 | if (argc != 2) 26 | { 27 | printf("Usage : %s \n", argv[0]); 28 | exit(1); 29 | } 30 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 31 | memset(&serv_adr, 0, sizeof(serv_adr)); 32 | serv_adr.sin_family = AF_INET; 33 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 34 | serv_adr.sin_port = htons(atoi(argv[1])); 35 | 36 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 37 | error_handling("bind() error"); 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | epfd = epoll_create(EPOLL_SIZE); //可以忽略这个参数,填入的参数为操作系统参考 42 | ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); 43 | 44 | event.events = EPOLLIN; //需要读取数据的情况 45 | event.data.fd = serv_sock; 46 | epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 47 | 48 | while (1) 49 | { 50 | event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //获取改变了的文件描述符,返回数量 51 | if (event_cnt == -1) 52 | { 53 | puts("epoll_wait() error"); 54 | break; 55 | } 56 | 57 | puts("return epoll_wait"); 58 | for (i = 0; i < event_cnt; i++) 59 | { 60 | if (ep_events[i].data.fd == serv_sock) //客户端请求连接时 61 | { 62 | adr_sz = sizeof(clnt_adr); 63 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 64 | event.events = EPOLLIN; 65 | event.data.fd = clnt_sock; //把客户端套接字添加进去 66 | epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 67 | printf("connected client : %d \n", clnt_sock); 68 | } 69 | else //是客户端套接字时 70 | { 71 | str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 72 | if (str_len == 0) 73 | { 74 | epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //从epoll中删除套接字 75 | close(ep_events[i].data.fd); 76 | printf("closed client : %d \n", ep_events[i].data.fd); 77 | } 78 | else 79 | { 80 | write(ep_events[i].data.fd, buf, str_len); 81 | } 82 | } 83 | } 84 | } 85 | close(serv_sock); 86 | close(epfd); 87 | 88 | return 0; 89 | } 90 | 91 | void error_handling(char *message) 92 | { 93 | fputs(message, stderr); 94 | fputc('\n', stderr); 95 | exit(1); 96 | } 97 | -------------------------------------------------------------------------------- /ch17/echo_epollserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 100 10 | #define EPOLL_SIZE 50 11 | void error_handling(char *message); 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | int serv_sock, clnt_sock; 16 | struct sockaddr_in serv_adr, clnt_adr; 17 | socklen_t adr_sz; 18 | int str_len, i; 19 | char buf[BUF_SIZE]; 20 | 21 | struct epoll_event *ep_events; 22 | struct epoll_event event; 23 | int epfd, event_cnt; 24 | 25 | if (argc != 2) 26 | { 27 | printf("Usage : %s \n", argv[0]); 28 | exit(1); 29 | } 30 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 31 | memset(&serv_adr, 0, sizeof(serv_adr)); 32 | serv_adr.sin_family = AF_INET; 33 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 34 | serv_adr.sin_port = htons(atoi(argv[1])); 35 | 36 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 37 | error_handling("bind() error"); 38 | if (listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | epfd = epoll_create(EPOLL_SIZE); //可以忽略这个参数,填入的参数为操作系统参考 42 | ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); 43 | 44 | event.events = EPOLLIN; //需要读取数据的情况 45 | event.data.fd = serv_sock; 46 | epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 47 | 48 | while (1) 49 | { 50 | event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //获取改变了的文件描述符,返回数量 51 | if (event_cnt == -1) 52 | { 53 | puts("epoll_wait() error"); 54 | break; 55 | } 56 | 57 | for (i = 0; i < event_cnt; i++) 58 | { 59 | if (ep_events[i].data.fd == serv_sock) //客户端请求连接时 60 | { 61 | adr_sz = sizeof(clnt_adr); 62 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 63 | event.events = EPOLLIN; 64 | event.data.fd = clnt_sock; //把客户端套接字添加进去 65 | epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 66 | printf("connected client : %d \n", clnt_sock); 67 | } 68 | else //是客户端套接字时 69 | { 70 | str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 71 | if (str_len == 0) 72 | { 73 | epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //从epoll中删除套接字 74 | close(ep_events[i].data.fd); 75 | printf("closed client : %d \n", ep_events[i].data.fd); 76 | } 77 | else 78 | { 79 | write(ep_events[i].data.fd, buf, str_len); 80 | } 81 | } 82 | } 83 | } 84 | close(serv_sock); 85 | close(epfd); 86 | 87 | return 0; 88 | } 89 | 90 | void error_handling(char *message) 91 | { 92 | fputs(message, stderr); 93 | fputc('\n', stderr); 94 | exit(1); 95 | } 96 | -------------------------------------------------------------------------------- /ch17/homework/char_EPETserv.c: -------------------------------------------------------------------------------- 1 | //边缘触发 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define BUF_SIZE 100 13 | #define MAX_CLNT 256 14 | #define EPOLL_SIZE 50 15 | 16 | void setnonblockingmode(int fd); 17 | void error_handling(char *buf); 18 | void send_msg(char * msg, int len); 19 | 20 | int clnt_cnt = 0; 21 | int clnt_socks[MAX_CLNT]; 22 | 23 | int main(int argc, char *argv[]) 24 | { 25 | int serv_sock, clnt_sock; 26 | struct sockaddr_in serv_adr, clnt_adr; 27 | socklen_t adr_sz; 28 | int str_len, i; 29 | char buf[BUF_SIZE]; 30 | 31 | struct epoll_event *ep_events; 32 | struct epoll_event event; 33 | int epfd, event_cnt; 34 | 35 | if(argc!=2) { 36 | printf("Usage : %s \n", argv[0]); 37 | exit(1); 38 | } 39 | 40 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 41 | memset(&serv_adr, 0, sizeof(serv_adr)); 42 | serv_adr.sin_family = AF_INET; 43 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 44 | serv_adr.sin_port = htons(atoi(argv[1])); 45 | 46 | if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) 47 | error_handling("bind() error"); 48 | if(listen(serv_sock, 5) == -1) 49 | error_handling("listen() error"); 50 | 51 | epfd = epoll_create(EPOLL_SIZE); 52 | ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE); 53 | 54 | setnonblockingmode(serv_sock); 55 | event.events = EPOLLIN; 56 | event.data.fd = serv_sock; 57 | epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); 58 | 59 | while(1) 60 | { 61 | event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); 62 | if(event_cnt == -1) 63 | { 64 | puts("epoll_wait() error"); 65 | break; 66 | } 67 | 68 | puts("return epoll_wait"); 69 | for(i = 0; i < event_cnt; ++i) 70 | { 71 | if(ep_events[i].data.fd == serv_sock) 72 | { 73 | adr_sz = sizeof(clnt_adr); 74 | clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz); 75 | setnonblockingmode(clnt_sock); 76 | event.events = EPOLLIN|EPOLLET; 77 | event.data.fd = clnt_sock; 78 | epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 79 | clnt_socks[clnt_cnt++] = clnt_sock; 80 | printf("connected client: %d \n", clnt_sock); 81 | } 82 | else 83 | { 84 | while(1) 85 | { 86 | str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 87 | if(str_len == 0) // close request! 88 | { 89 | epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); 90 | close(ep_events[i].data.fd); 91 | 92 | for(i = 0; i < clnt_cnt; ++i) 93 | { 94 | if(ep_events[i].data.fd == clnt_socks[i]) 95 | { 96 | while(i++ < clnt_cnt-1) 97 | clnt_socks[i] = clnt_socks[i+1]; 98 | break; 99 | } 100 | } 101 | --clnt_cnt; 102 | printf("closed client: %d \n", ep_events[i].data.fd); 103 | break; 104 | } 105 | else if(str_len < 0) 106 | { 107 | if(errno == EAGAIN) 108 | break; 109 | } 110 | else 111 | { 112 | send_msg(buf, str_len); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | close(serv_sock); 119 | close(epfd); 120 | return 0; 121 | } 122 | 123 | 124 | void send_msg(char * msg, int len) // send to all 125 | { 126 | int i; 127 | for(i = 0; i < clnt_cnt; ++i) 128 | write(clnt_socks[i], msg, len); 129 | } 130 | 131 | //设置为非阻塞模式 132 | void setnonblockingmode(int fd) 133 | { 134 | int flag = fcntl(fd, F_GETFL, 0); 135 | fcntl(fd, F_SETFL, flag|O_NONBLOCK); 136 | } 137 | 138 | void error_handling(char *buf) 139 | { 140 | fputs(buf, stderr); 141 | fputc('\n', stderr); 142 | exit(1); 143 | } 144 | -------------------------------------------------------------------------------- /ch17/homework/char_EPLTserv.c: -------------------------------------------------------------------------------- 1 | //条件触发 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define BUF_SIZE 100 12 | #define MAX_CLNT 256 13 | #define EPOLL_SIZE 50 14 | void error_handling(char *buf); 15 | void send_msg(char *msg, int len); 16 | 17 | int clnt_cnt = 0; 18 | int clnt_socks[MAX_CLNT]; 19 | 20 | int main(int argc, char *argv[]) 21 | { 22 | int serv_sock, clnt_sock; 23 | struct sockaddr_in serv_adr, clnt_adr; 24 | socklen_t adr_sz; 25 | int str_len, i; 26 | char buf[BUF_SIZE]; 27 | 28 | struct epoll_event *ep_events; 29 | struct epoll_event event; 30 | int epfd, event_cnt; 31 | 32 | if(argc != 2) { 33 | printf("Usage: %s \n", argv[0]); 34 | exit(1); 35 | } 36 | 37 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 38 | memset(&serv_adr, 0, sizeof(serv_adr)); 39 | serv_adr.sin_family = AF_INET; 40 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 41 | serv_adr.sin_port = htons(atoi(argv[1])); 42 | 43 | if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) 44 | error_handling("bind() error"); 45 | if(listen(serv_sock, 5) == -1) 46 | error_handling("listen() error"); 47 | 48 | //创建epoll例程 49 | epfd = epoll_create(EPOLL_SIZE); 50 | //动态分配内存 51 | ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE); 52 | 53 | event.events = EPOLLIN;//事件类型为需要读取的情况 54 | event.data.fd = serv_sock; 55 | //在epoll例程中注册服务端套接字 56 | epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); 57 | 58 | while(1) 59 | { 60 | //获取发生改变的文件描述符数量 61 | event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); 62 | if(event_cnt == -1) 63 | break; 64 | 65 | for(i = 0; i < event_cnt; ++i) 66 | { 67 | if(ep_events[i].data.fd == serv_sock)//serv_sock 68 | { 69 | adr_sz = sizeof(clnt_adr); 70 | clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_adr, &adr_sz); 71 | event.events = EPOLLIN; 72 | event.data.fd = clnt_sock; 73 | //在epoll例程中注册clnt_sock套接字 74 | epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 75 | clnt_socks[clnt_cnt++] = clnt_sock; 76 | printf("connected client: %d \n", clnt_sock); 77 | } 78 | else //clnt_sock 79 | { 80 | str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 81 | if(str_len == 0) 82 | { 83 | //在例程中删除clnt_sock套接字 84 | epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); 85 | //关闭clnt_sock套接字 86 | close(ep_events[i].data.fd); 87 | printf("closed client: %d \n", ep_events[i].data.fd); 88 | 89 | for(i = 0; i < clnt_cnt; ++i) 90 | { 91 | if(clnt_sock == clnt_socks[i]) 92 | { 93 | while(i++ < clnt_cnt - 1) 94 | clnt_socks[i] = clnt_socks[i + 1]; 95 | break; 96 | } 97 | } 98 | --clnt_cnt; 99 | } 100 | else 101 | { 102 | send_msg(buf, str_len); 103 | } 104 | } 105 | } 106 | } 107 | close(serv_sock); 108 | close(epfd); 109 | return 0; 110 | } 111 | 112 | void send_msg(char * msg, int len) 113 | { 114 | int i; 115 | for(i = 0; i < clnt_cnt; ++i) 116 | write(clnt_socks[i], msg, len); 117 | } 118 | 119 | void error_handling(char *buf) 120 | { 121 | fputs(buf, stderr); 122 | fputc('\n', stderr); 123 | exit(1); 124 | } -------------------------------------------------------------------------------- /ch17/homework/chat_clnt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 100 10 | #define NAME_SIZE 20 11 | 12 | void * send_msg(void * arg); 13 | void * recv_msg(void * arg); 14 | void error_handling(char * msg); 15 | 16 | char name[NAME_SIZE] = "[DEFAULT]"; 17 | char msg[BUF_SIZE]; 18 | 19 | int main(int argc, char *argv[]) 20 | { 21 | int sock; 22 | struct sockaddr_in adr; 23 | pthread_t send_thread, recv_thread; 24 | void * thread_return;//线程返回值 25 | if(argc != 4) { 26 | printf("Usage: %s \n", argv[0]); 27 | exit(1); 28 | } 29 | 30 | sprintf(name, "[%s]", argv[3]);//把名字写入name数组 31 | sock = socket(PF_INET, SOCK_STREAM, 0); 32 | 33 | memset(&adr, 0, sizeof(adr)); 34 | adr.sin_family = AF_INET; 35 | adr.sin_addr.s_addr = inet_addr(argv[1]); 36 | adr.sin_port = htons(atoi(argv[2])); 37 | 38 | if(connect(sock, (struct sockaddr*) &adr, sizeof(adr)) == -1) 39 | error_handling("connect() error"); 40 | /*线程创建*/ 41 | pthread_create(&send_thread, NULL, send_msg, (void *)&sock); 42 | pthread_create(&recv_thread, NULL, recv_msg, (void *)&sock); 43 | /*进程阻塞,进入发送消息的线程*/ 44 | pthread_join(send_thread, &thread_return); 45 | /*进程阻塞,进入接收消息的线程*/ 46 | pthread_join(recv_thread, &thread_return); 47 | close(sock); 48 | return 0; 49 | } 50 | 51 | void * send_msg(void * arg) 52 | { 53 | int sock = *((int *)arg); 54 | char name_msg[NAME_SIZE + BUF_SIZE]; 55 | 56 | while(1) 57 | { 58 | fputs("Input message(q to quit):\n", stdout); 59 | fgets(msg, BUF_SIZE, stdin); 60 | if(!(strcmp(msg, "q\n")) || !strcmp(msg, "Q\n")) 61 | { 62 | close(sock); 63 | exit(0); 64 | } 65 | sprintf(name_msg, "%s %s", name, msg); 66 | write(sock, name_msg, strlen(name_msg));//strlen()函数碰到'\0'会结束 67 | } 68 | return NULL; 69 | } 70 | 71 | void * recv_msg(void * arg) 72 | { 73 | int sock = *((int *)arg); 74 | char name_msg[NAME_SIZE + BUF_SIZE]; 75 | int str_len; 76 | 77 | while(1) 78 | { 79 | str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1); 80 | if(str_len == -1) 81 | return (void *)-1; 82 | name_msg[str_len] = 0; 83 | fputs(name_msg, stdout); 84 | } 85 | return NULL; 86 | } 87 | 88 | void error_handling(char *msg) 89 | { 90 | fputs(msg, stderr); 91 | fputc('\n', stderr); 92 | exit(1); 93 | } -------------------------------------------------------------------------------- /ch18/chat_clnt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 100 10 | #define NAME_SIZE 20 11 | 12 | void *send_msg(void *arg); 13 | void *recv_msg(void *arg); 14 | void error_handling(char *msg); 15 | 16 | char name[NAME_SIZE] = "[DEFAULT]"; 17 | char msg[BUF_SIZE]; 18 | 19 | int main(int argc, char *argv[]) 20 | { 21 | int sock; 22 | struct sockaddr_in serv_addr; 23 | pthread_t snd_thread, rcv_thread; 24 | void *thread_return; 25 | if (argc != 4) 26 | { 27 | printf("Usage : %s \n", argv[0]); 28 | exit(1); 29 | } 30 | 31 | sprintf(name, "[%s]", argv[3]); 32 | sock = socket(PF_INET, SOCK_STREAM, 0); 33 | 34 | memset(&serv_addr, 0, sizeof(serv_addr)); 35 | serv_addr.sin_family = AF_INET; 36 | serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 37 | serv_addr.sin_port = htons(atoi(argv[2])); 38 | 39 | if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 40 | error_handling("connect() error"); 41 | 42 | pthread_create(&snd_thread, NULL, send_msg, (void *)&sock); //创建发送消息线程 43 | pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock); //创建接受消息线程 44 | pthread_join(snd_thread, &thread_return); 45 | pthread_join(rcv_thread, &thread_return); 46 | close(sock); 47 | return 0; 48 | } 49 | 50 | void *send_msg(void *arg) // 发送消息 51 | { 52 | int sock = *((int *)arg); 53 | char name_msg[NAME_SIZE + BUF_SIZE]; 54 | while (1) 55 | { 56 | fgets(msg, BUF_SIZE, stdin); 57 | if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) 58 | { 59 | close(sock); 60 | exit(0); 61 | } 62 | sprintf(name_msg, "%s %s", name, msg); 63 | write(sock, name_msg, strlen(name_msg)); 64 | } 65 | return NULL; 66 | } 67 | 68 | void *recv_msg(void *arg) // 读取消息 69 | { 70 | int sock = *((int *)arg); 71 | char name_msg[NAME_SIZE + BUF_SIZE]; 72 | int str_len; 73 | while (1) 74 | { 75 | str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1); 76 | if (str_len == -1) 77 | return (void *)-1; 78 | name_msg[str_len] = 0; 79 | fputs(name_msg, stdout); 80 | } 81 | return NULL; 82 | } 83 | 84 | void error_handling(char *msg) 85 | { 86 | fputs(msg, stderr); 87 | fputc('\n', stderr); 88 | exit(1); 89 | } -------------------------------------------------------------------------------- /ch18/chat_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 100 11 | #define MAX_CLNT 256 12 | 13 | void *handle_clnt(void *arg); 14 | void send_msg(char *msg, int len); 15 | void error_handling(char *msg); 16 | 17 | int clnt_cnt = 0; 18 | int clnt_socks[MAX_CLNT]; 19 | pthread_mutex_t mutx; 20 | 21 | int main(int argc, char *argv[]) 22 | { 23 | int serv_sock, clnt_sock; 24 | struct sockaddr_in serv_adr, clnt_adr; 25 | int clnt_adr_sz; 26 | pthread_t t_id; 27 | if (argc != 2) 28 | { 29 | printf("Usage : %s \n", argv[0]); 30 | exit(1); 31 | } 32 | 33 | pthread_mutex_init(&mutx, NULL); //创建互斥锁 34 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 35 | 36 | memset(&serv_adr, 0, sizeof(serv_adr)); 37 | serv_adr.sin_family = AF_INET; 38 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 39 | serv_adr.sin_port = htons(atoi(argv[1])); 40 | 41 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 42 | error_handling("bind() error"); 43 | if (listen(serv_sock, 5) == -1) 44 | error_handling("listen() error"); 45 | 46 | while (1) 47 | { 48 | clnt_adr_sz = sizeof(clnt_adr); 49 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); 50 | 51 | pthread_mutex_lock(&mutx); //上锁 52 | clnt_socks[clnt_cnt++] = clnt_sock; //写入新连接 53 | pthread_mutex_unlock(&mutx); //解锁 54 | 55 | pthread_create(&t_id, NULL, handle_clnt, (void *)&clnt_sock); //创建线程为新客户端服务,并且把clnt_sock作为参数传递 56 | pthread_detach(t_id); //引导线程销毁,不阻塞 57 | printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); //客户端连接的ip地址 58 | } 59 | close(serv_sock); 60 | return 0; 61 | } 62 | 63 | void *handle_clnt(void *arg) 64 | { 65 | int clnt_sock = *((int *)arg); 66 | int str_len = 0, i; 67 | char msg[BUF_SIZE]; 68 | 69 | while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0) 70 | send_msg(msg, str_len); 71 | //接收到消息为0,代表当前客户端已经断开连接 72 | pthread_mutex_lock(&mutx); 73 | for (i = 0; i < clnt_cnt; i++) //删除没有连接的客户端 74 | { 75 | if (clnt_sock == clnt_socks[i]) 76 | { 77 | while (i++ < clnt_cnt - 1) 78 | clnt_socks[i] = clnt_socks[i + 1]; 79 | break; 80 | } 81 | } 82 | clnt_cnt--; 83 | pthread_mutex_unlock(&mutx); 84 | close(clnt_sock); 85 | return NULL; 86 | } 87 | void send_msg(char *msg, int len) //向连接的所有客户端发送消息 88 | { 89 | int i; 90 | pthread_mutex_lock(&mutx); 91 | for (i = 0; i < clnt_cnt; i++) 92 | write(clnt_socks[i], msg, len); 93 | pthread_mutex_unlock(&mutx); 94 | } 95 | void error_handling(char *msg) 96 | { 97 | fputs(msg, stderr); 98 | fputc('\n', stderr); 99 | exit(1); 100 | } -------------------------------------------------------------------------------- /ch18/homework/echo_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define BUF_SIZE 1024 8 | void error_handling(char* message); 9 | 10 | int main(int argc, char* argv[]) 11 | { 12 | int sock; 13 | char message[BUF_SIZE]; 14 | int str_len; 15 | struct sockaddr_in serv_adr; 16 | 17 | if(argc != 3) { 18 | printf("Usage : %s \n", argv[0]); 19 | exit(1); 20 | } 21 | /*创建tcp套接字*/ 22 | sock = socket(PF_INET, SOCK_STREAM, 0); 23 | if(sock == -1) 24 | error_handling("socket() error"); 25 | /*初始化*/ 26 | memset(&serv_adr, 0, sizeof(serv_adr)); 27 | serv_adr.sin_family = AF_INET; 28 | serv_adr.sin_addr.s_addr = inet_addr(argv[1]);//string转大端序整型 29 | serv_adr.sin_port = htons(atoi(argv[2])); 30 | /*建立连接*/ 31 | if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) 32 | error_handling("connect() error"); 33 | else 34 | puts("Connected..........."); 35 | 36 | while(1) 37 | { 38 | fputs("Input message(Q to quit): ", stdout); 39 | fgets(message, BUF_SIZE, stdin); 40 | //实现按q或者Q退出 41 | if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) 42 | break; 43 | 44 | write(sock, message, strlen(message)); 45 | str_len = read(sock, message, BUF_SIZE - 1); 46 | message[str_len] = 0; 47 | printf("Message from server: %s", message); 48 | } 49 | close(sock); 50 | return 0; 51 | } 52 | 53 | void error_handling(char* message) 54 | { 55 | fputs(message, stderr); 56 | fputc('\n',stderr); 57 | exit(1); 58 | } -------------------------------------------------------------------------------- /ch18/homework/echo_threadserv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define BUF_SIZE 100 11 | void * handle_clnt(void * arg); 12 | void error_handling(char *buf); 13 | 14 | char buf[BUF_SIZE]; 15 | pthread_mutex_t mutx; 16 | 17 | int main(int argc, char *argv[]) 18 | { 19 | int serv_sock, clnt_sock; 20 | struct sockaddr_in serv_adr, clnt_adr; 21 | int clnt_adr_sz; 22 | pthread_t t_id; 23 | 24 | if(argc != 2) { 25 | printf("Usage : %s \n", argv[0]); 26 | exit(1); 27 | } 28 | 29 | pthread_mutex_init(&mutx, NULL); 30 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 31 | memset(&serv_adr, 0, sizeof(serv_adr)); 32 | serv_adr.sin_family = AF_INET; 33 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 34 | serv_adr.sin_port = htons(atoi(argv[1])); 35 | 36 | if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) 37 | error_handling("bind() error"); 38 | if(listen(serv_sock, 5) == -1) 39 | error_handling("listen() error"); 40 | 41 | while(1) 42 | { 43 | clnt_adr_sz = sizeof(clnt_adr); 44 | clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz); 45 | pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock); 46 | pthread_detach(t_id); 47 | printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); 48 | } 49 | 50 | close(serv_sock); 51 | return 0; 52 | } 53 | 54 | void * handle_clnt(void * arg) 55 | { 56 | int clnt_sock = *((int*)arg); 57 | int str_len = 0; 58 | 59 | while(1) 60 | { 61 | pthread_mutex_lock(&mutx); 62 | str_len = read(clnt_sock, buf, sizeof(buf)); 63 | if(str_len <= 0) 64 | break; 65 | else 66 | write(clnt_sock, buf, str_len); 67 | pthread_mutex_unlock(&mutx); 68 | } 69 | 70 | close(clnt_sock); 71 | return NULL; 72 | } 73 | void error_handling(char *buf) 74 | { 75 | fputs(buf, stderr); 76 | fputc('\n', stderr); 77 | exit(1); 78 | } 79 | -------------------------------------------------------------------------------- /ch18/mutex.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #define NUM_THREAD 100 6 | void *thread_inc(void *arg); 7 | void *thread_des(void *arg); 8 | 9 | long long num = 0; 10 | pthread_mutex_t mutex; //保存互斥量读取值的变量 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | pthread_t thread_id[NUM_THREAD]; 15 | int i; 16 | 17 | pthread_mutex_init(&mutex, NULL); //创建互斥量 18 | 19 | for (i = 0; i < NUM_THREAD; i++) 20 | { 21 | if (i % 2) 22 | pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); 23 | else 24 | pthread_create(&(thread_id[i]), NULL, thread_des, NULL); 25 | } 26 | 27 | for (i = 0; i < NUM_THREAD; i++) 28 | pthread_join(thread_id[i], NULL); 29 | 30 | printf("result: %lld \n", num); 31 | pthread_mutex_destroy(&mutex); //销毁互斥量 32 | return 0; 33 | } 34 | 35 | void *thread_inc(void *arg) 36 | { 37 | int i; 38 | pthread_mutex_lock(&mutex); //上锁 39 | for (i = 0; i < 50000000; i++) 40 | num += 1; 41 | pthread_mutex_unlock(&mutex); //解锁 42 | return NULL; 43 | } 44 | void *thread_des(void *arg) 45 | { 46 | int i; 47 | pthread_mutex_lock(&mutex); 48 | for (i = 0; i < 50000000; i++) 49 | num -= 1; 50 | pthread_mutex_unlock(&mutex); 51 | return NULL; 52 | } -------------------------------------------------------------------------------- /ch18/semaphore.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void *read(void *arg); 6 | void *accu(void *arg); 7 | static sem_t sem_one; 8 | static sem_t sem_two; 9 | static int num; 10 | 11 | int main(int argc, char const *argv[]) 12 | { 13 | pthread_t id_t1, id_t2; 14 | sem_init(&sem_one, 0, 0); 15 | sem_init(&sem_two, 0, 1); 16 | 17 | pthread_create(&id_t1, NULL, read, NULL); 18 | pthread_create(&id_t2, NULL, accu, NULL); 19 | 20 | pthread_join(id_t1, NULL); 21 | pthread_join(id_t2, NULL); 22 | 23 | sem_destroy(&sem_one); 24 | sem_destroy(&sem_two); 25 | return 0; 26 | } 27 | 28 | void *read(void *arg) 29 | { 30 | int i; 31 | for (i = 0; i < 5; i++) 32 | { 33 | fputs("Input num: ", stdout); 34 | 35 | sem_wait(&sem_two); 36 | scanf("%d", &num); 37 | sem_post(&sem_one); 38 | } 39 | return NULL; 40 | } 41 | void *accu(void *arg) 42 | { 43 | int sum = 0, i; 44 | for (i = 0; i < 5; i++) 45 | { 46 | sem_wait(&sem_one); 47 | sum += num; 48 | sem_post(&sem_two); 49 | } 50 | printf("Result: %d \n", sum); 51 | return NULL; 52 | } -------------------------------------------------------------------------------- /ch18/thread1.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | void *thread_main(void *arg); 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | pthread_t t_id; 9 | int thread_param = 5; 10 | // 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行。同时传递参数 11 | if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0) 12 | { 13 | puts("pthread_create() error"); 14 | return -1; 15 | } 16 | sleep(10); //延迟进程终止时间 17 | puts("end of main"); 18 | return 0; 19 | } 20 | void *thread_main(void *arg) //传入的参数是 pthread_create 的第四个 21 | { 22 | int i; 23 | int cnt = *((int *)arg); 24 | for (int i = 0; i < cnt; i++) 25 | { 26 | sleep(1); 27 | puts("running thread"); 28 | } 29 | return NULL; 30 | } -------------------------------------------------------------------------------- /ch18/thread2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | void *thread_main(void *arg); 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | pthread_t t_id; 10 | int thread_param = 5; 11 | void *thr_ret; 12 | // 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行。同时传递参数 13 | if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0) 14 | { 15 | puts("pthread_create() error"); 16 | return -1; 17 | } 18 | //main函数将等待 ID 保存在 t_id 变量中的线程终止 19 | if (pthread_join(t_id, &thr_ret) != 0) 20 | { 21 | puts("pthread_join() error"); 22 | return -1; 23 | } 24 | printf("Thread return message : %s \n", (char *)thr_ret); 25 | free(thr_ret); 26 | return 0; 27 | } 28 | void *thread_main(void *arg) //传入的参数是 pthread_create 的第四个 29 | { 30 | int i; 31 | int cnt = *((int *)arg); 32 | char *msg = (char *)malloc(sizeof(char) * 50); 33 | strcpy(msg, "Hello,I'am thread~ \n"); 34 | for (int i = 0; i < cnt; i++) 35 | { 36 | sleep(1); 37 | puts("running thread"); 38 | } 39 | return (void *)msg; //返回值是 thread_main 函数中内部动态分配的内存空间地址值 40 | } -------------------------------------------------------------------------------- /ch18/thread3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | void *thread_summation(void *arg); 4 | int sum = 0; 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | pthread_t id_t1, id_t2; 9 | int range1[] = {1, 5}; 10 | int range2[] = {6, 10}; 11 | 12 | pthread_create(&id_t1, NULL, thread_summation, (void *)range1); 13 | pthread_create(&id_t2, NULL, thread_summation, (void *)range2); 14 | 15 | pthread_join(id_t1, NULL); 16 | pthread_join(id_t2, NULL); 17 | printf("result: %d \n", sum); 18 | return 0; 19 | } 20 | void *thread_summation(void *arg) 21 | { 22 | int start = ((int *)arg)[0]; 23 | int end = ((int *)arg)[1]; 24 | while (start <= end) 25 | { 26 | sum += start; 27 | start++; 28 | } 29 | return NULL; 30 | } -------------------------------------------------------------------------------- /ch18/thread4.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #define NUM_THREAD 100 6 | 7 | void *thread_inc(void *arg); 8 | void *thread_des(void *arg); 9 | long long num = 0; 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | pthread_t thread_id[NUM_THREAD]; 14 | int i; 15 | 16 | printf("sizeof long long: %d \n", sizeof(long long)); 17 | for (i = 0; i < NUM_THREAD; i++) 18 | { 19 | if (i % 2) 20 | pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); 21 | else 22 | pthread_create(&(thread_id[i]), NULL, thread_des, NULL); 23 | } 24 | 25 | for (i = 0; i < NUM_THREAD; i++) 26 | pthread_join(thread_id[i], NULL); 27 | 28 | printf("result: %lld \n", num); 29 | return 0; 30 | } 31 | 32 | void *thread_inc(void *arg) 33 | { 34 | int i; 35 | for (i = 0; i < 50000000; i++) 36 | num += 1; 37 | return NULL; 38 | } 39 | void *thread_des(void *arg) 40 | { 41 | int i; 42 | for (i = 0; i < 50000000; i++) 43 | num -= 1; 44 | return NULL; 45 | } -------------------------------------------------------------------------------- /ch24/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 测试 6 | 7 | 8 |

测试标题

9 |

这是一个段落

10 | 11 | -------------------------------------------------------------------------------- /ch24/webserv_linux.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUF_SIZE 1024 10 | #define SMALL_BUF 100 11 | 12 | void *request_handler(void *arg); 13 | void send_data(FILE *fp, char *ct, char *file_name); 14 | char *content_type(char *file); 15 | void send_error(FILE *fp); 16 | void error_handling(char *message); 17 | 18 | int main(int argc, char *argv[]) 19 | { 20 | int serv_sock, clnt_sock; 21 | struct sockaddr_in serv_adr, clnt_adr; 22 | int clnt_adr_size; 23 | char buf[BUF_SIZE]; 24 | pthread_t t_id; 25 | if (argc != 2) 26 | { 27 | printf("Usage : %s \n", argv[0]); 28 | exit(1); 29 | } 30 | 31 | serv_sock = socket(PF_INET, SOCK_STREAM, 0); 32 | memset(&serv_adr, 0, sizeof(serv_adr)); 33 | serv_adr.sin_family = AF_INET; 34 | serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 35 | serv_adr.sin_port = htons(atoi(argv[1])); 36 | if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) 37 | error_handling("bind() error"); 38 | if (listen(serv_sock, 20) == -1) 39 | error_handling("listen() error"); 40 | 41 | while (1) 42 | { 43 | clnt_adr_size = sizeof(clnt_adr); 44 | clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_size); 45 | printf("Connection Request : %s:%d\n", 46 | inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port)); 47 | pthread_create(&t_id, NULL, request_handler, &clnt_sock); 48 | pthread_detach(t_id); 49 | } 50 | close(serv_sock); 51 | return 0; 52 | } 53 | 54 | void *request_handler(void *arg) 55 | { 56 | int clnt_sock = *((int *)arg); 57 | char req_line[SMALL_BUF]; 58 | FILE *clnt_read; 59 | FILE *clnt_write; 60 | 61 | char method[10]; 62 | char ct[15]; 63 | char file_name[30]; 64 | 65 | clnt_read = fdopen(clnt_sock, "r"); 66 | clnt_write = fdopen(dup(clnt_sock), "w"); 67 | fgets(req_line, SMALL_BUF, clnt_read); 68 | if (strstr(req_line, "HTTP/") == NULL) 69 | { 70 | send_error(clnt_write); 71 | fclose(clnt_read); 72 | fclose(clnt_write); 73 | return; 74 | } 75 | strcpy(method, strtok(req_line, " /")); 76 | strcpy(file_name, strtok(NULL, " /")); 77 | strcpy(ct, content_type(file_name)); 78 | if (strcmp(method, "GET") != 0) 79 | { 80 | send_error(clnt_write); 81 | fclose(clnt_read); 82 | fclose(clnt_write); 83 | return; 84 | } 85 | fclose(clnt_read); 86 | send_data(clnt_write, ct, file_name); 87 | } 88 | void send_data(FILE *fp, char *ct, char *file_name) 89 | { 90 | char protocol[] = "HTTP/1.0 200 OK\r\n"; 91 | char server[] = "Server:Linux Web Server \r\n"; 92 | char cnt_len[] = "Content-length:2048\r\n"; 93 | char cnt_type[SMALL_BUF]; 94 | char buf[BUF_SIZE]; 95 | FILE *send_file; 96 | 97 | sprintf(cnt_type, "Content-type:%s\r\n\r\n", ct); 98 | send_file = fopen(file_name, "r"); 99 | if (send_file == NULL) 100 | { 101 | send_error(fp); 102 | return; 103 | } 104 | 105 | //传输头信息 106 | fputs(protocol, fp); 107 | fputs(server, fp); 108 | fputs(cnt_len, fp); 109 | fputs(cnt_type, fp); 110 | 111 | //传输请求数据 112 | while (fgets(buf, BUF_SIZE, send_file) != NULL) 113 | { 114 | fputs(buf, fp); 115 | fflush(fp); 116 | } 117 | fflush(fp); 118 | fclose(fp); 119 | } 120 | char *content_type(char *file) 121 | { 122 | char extension[SMALL_BUF]; 123 | char file_name[SMALL_BUF]; 124 | strcpy(file_name, file); 125 | strtok(file_name, "."); 126 | strcpy(extension, strtok(NULL, ".")); 127 | 128 | if (!strcmp(extension, "html") || !strcmp(extension, "htm")) 129 | return "text/html"; 130 | else 131 | return "text/plain"; 132 | } 133 | void send_error(FILE *fp) 134 | { 135 | char protocol[] = "HTTP/1.0 400 Bad Request\r\n"; 136 | char server[] = "Server:Linux Web Server \r\n"; 137 | char cnt_len[] = "Content-length:2048\r\n"; 138 | char cnt_type[] = "Content-type:text/html\r\n\r\n"; 139 | char content[] = "NETWORK" 140 | "
发生错误! 查看请求文件名和请求方式!" 141 | "
"; 142 | fputs(protocol, fp); 143 | fputs(server, fp); 144 | fputs(cnt_len, fp); 145 | fputs(cnt_type, fp); 146 | fflush(fp); 147 | } 148 | void error_handling(char *message) 149 | { 150 | fputs(message, stderr); 151 | fputc('\n', stderr); 152 | exit(1); 153 | } --------------------------------------------------------------------------------