说明
学习自

- https://github.com/834810071/NetworkProgramming
- https://www.bilibili.com/video/av33813135/
我主要的学习目标:
- 固定住 socket 编程的基础知识
- 如何使用 进程 编程
- 如何使用 thread 编程
- p1 讲解了,网络模型,理解了四层的模型
p4
- tcp 基于字节流
- 无边界的字节流,需要对数据进行分割
- 粘包问题,解决方法:
- 发送方: 发送数据前,先将数据包的长度信息发送给对方
- 接收方: 接收到数据包长度信息后,根据长度信息,将数据包拆分为多个数据包进行处理
面向连接的 tcp 协议,需要建立连接,断开连接,保证数据传输的可靠性 缓冲传输
tcp报文格式
- 紧急指针: 紧急数据指针,只有紧急数据才会使用紧急指针,紧急数据是指需要立即传输的数据
- 选项字段: 选项字段,可以携带一些控制信息,如窗口大小,最大报文段长度,时间戳等
tcp三次握手
- 第一次握手: 客户端发送 SYN 报文,服务器收到后,发送 SYN+ACK 报文,客户端收到后,发送 ACK 报文
- 第二次握手: 客户端发送 ACK 报文,服务器收到后,发送 ACK 报文
- 第三次握手: 客户端发送 ACK 报文,服务器收到后,连接建立完成
tcp四次挥手
- 第一次挥手: 客户端发送 FIN 报文,服务器收到后,发送 ACK 报文,客户端收到后,进入 TIME-WAIT 状态
- 第二次挥手: 服务器发送 FIN 报文,客户端收到后,发送 ACK 报文,服务器收到后,进入 CLOSED 状态
- 第三次挥手: 服务器发送 ACK 报文,客户端收到后,进入 CLOSED 状态
- 第四次挥手: 客户端发送 ACK 报文,服务器收到后,进入 CLOSED 状态
tcp拥塞控制
- 慢启动: 开始时,发送方的发送速率较低,逐渐增大,以便逼近网络的实际速率
- 拥塞避免: 网络拥塞时,发送方的发送速率降低,以防止网络过载
- 快速重传: 接收方收到重复的 ACK 报文,认为是丢包,立即重传丢失的报文段
- 超时重传: 发送方在一段时间内没有收到 ACK 报文,认为报文丢失,重新发送
- 快速恢复: 当网络拥塞程度减缓时,发送方的发送速率增加,以便减轻网络拥塞
滑动窗口协议
- 发送方维护一个窗口,指示自己可以发送的报文段数量
- 接收方维护一个窗口,指示自己可以接收的报文段数量
- 发送方根据接收方的反馈,调整窗口的大小,以便在网络拥塞时,减少发送报文段的数量
- 接收方根据发送方的反馈,调整窗口的大小,以便在网络拥塞时,减少接收报文段的数量
学习目标
mindmap
root((学习目标))
1.什么是socket
2.IPv4套接口地址结构
3.网终字节序
4.字节序转换函数
5.地址转换函数
6.套接字类型
1. 什么是socket
- socket可以看成是用户进程与内核网络协议栈的 编程接口
- socket不仅可以用于本机的进程间通信,还可以 用于网络上不同主机的进程间通信
我的理解: socket就是对前面学习的tcp/ip的代码封装,但是位于系统的内核,也就是说提供给用户的一组API,用于网络通信.
warning
socket 这里理解成"插座"
mindmap
root((socket需要的属性))
地址: sockaddr_in
sin_addr
sin_port
sin_family
ipv4 socket 地址结构体
ipv4 socket地址结构体,在头文件netinet/in.h中定义。 sockaddr_in结构体定义如下:
使用man 7 ip命令查看sockaddr_in结构体的定义:
struct sockaddr_in {
sa_family_t sin_family; // 地址族,ipv4 就是AF_INET
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 未使用,注意这个在man 7 ip中没有说明
//暗示程序员不需要知道这个??
//当成上面的普通的来用?
};
sin_family字段表示地址族,socket在设计的时候还可以用于其它的协议,不仅是tcp/ip,比如还有unix域,对于IPv4地址,该值为AF_INET。sin_port字段表示端口号,使用网络字节序(big-endian)存储。sin_addr字段表示IP地址,使用struct in_addr结构体表示。
struct in_addr结构体定义如下:
struct in_addr {
uint32_t s_addr; // IP地址,使用网络字节序存储
};
s_addr字段表示IP地址,使用网络字节序存储。
疑问1
note
到这里就有一个疑问,socketaddr_in结构体具体的定义是什么呢?后面有没有char sin_zero[8];字段呢?通过下面的代码来验证
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
int main (int argc, char *argv[]) {
printf("size bytes of sockaddr_in %ld\n",sizeof(struct sockaddr_in));
printf("size bytes of sin_family %ld\n",sizeof(sa_family_t));
printf("size bytes of sin_port %ld\n",sizeof(in_port_t));
printf("size bytes of sin_addr %ld\n",sizeof(struct in_addr));
return 0;
}
输出的结果是:16,
size bytes of sockaddr_in 16
size bytes of sin_family 2
size bytes of sin_port 2
size bytes of sin_addr 4
结果确实是少8
通用套接字地址结构体sockaddr
定义如下:
它适用于任何地址协议,
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址数据,长度取决于地址族
};
sa_family字段表示地址族。sa_data字段表示地址数据,长度取决于地址族。
#include <stdio.h>
#include <netinet/in.h>
int main(int argc, char const *argv[])
{
struct sockaddr_in addr1;
struct sockaddr addr2;
printf("sizof(sockaddr_in) = %ld\n", sizeof(addr1));
printf("sizof(sockaddr) = %ld\n", sizeof(addr2));
// 最后输出的结果是:
// sizof(sockaddr_in) = 16
// sizof(sockaddr) = 16
// 说明sockaddr_in和sockaddr的大小是相同的,都是16字节。
return 0;
}
在通常的网络编程中,我们先填写sockaddr_in结构体,然后再把它转换成通用的sockaddr结构体.
网络字节序
网络字节序
字节序分为:
- 大端字节序(Big-endian)
- 小端字节序(Little-endian)

网络字节序(Network Byte Order)是指数据的高位字节保存在内存的低地址中,而数据的低位字节保存在内存的高地址中。就是大端字节序.
在TCP/IP协议中,所有长度字段和数字都采用网络字节序。例如,在TCP报文头部的源端口字段中,端口号的高位字节保存在内存的低地址中,而端口号的低位字节保存在内存的高地址中。
说明
作者: 韩国 - 尹圣雨

学习资源
第一章 理解网络编程和socket
![INFO] 网络编程就是编写程序使用两台连网的计算机相互交换数据.
这就是整个网络编程的全部内容.我觉得不需要去详细的理解TCP/IP的细节.
下面我按我的理解这一章的内容.
如果我们想到在电脑上创建一个server端的软件,用来接收其它电脑上另一个程序通过网络发来的信息,应该如何做呢? 应该按下面的流程来做
---
title: server socket 创建流程
---
flowchart TD
step_1[1.创建socket,API:socket]
step_2[2.绑定端口与ip: bind]
step_3[3.开启监听 listen]
step_4[4.接受产生的socket: accept ]
step_1 --> step_2 --> step_3 --> step_4;
第一步创建 socket(插座)

如果我们家里需要网络通信,首先要有一个网络面板(网络插座)用来连接网络,然后才能通信. 同样我们也需要先创建一个socket,这个socket的作用就是用于后面的网络连接.
int socket(int domain,int type,int protocal)
具体参数的含义与理解见一下章节
第二步 bind函数
通过第一步我们创建了一个socket(插座),那么应该把它放在哪里呢? 其实就是根据哪些信息可以找到这个socket.
- 信息首先要能发送给电脑,也就是需要知道电脑的IP,一个电脑可以有多个IP.

比如这个电脑就有两个网口,(单网口也可以有多IP)
- 可以想像到一个电脑上能进行网络通信的软件有多个,那么就有很多个socket.



电脑为了和外部通信,创建了端口这个概念.
你可以把端口想像成电脑一个一个孔洞,每一个数据通过这个孔洞发流入进电脑,如何你的socket"安装"在这个端口上,那么就可以接收到这个数据了.
然后这个socket和还需要和某一端口绑定在一起.
#include <sys/socket.h>
int bind(int sockfd,struct sockaddr_in * myaddr,socklen_t addrlen)
第三步: 激活监听,listen
现在我们有了一个socket,并把它放到了合适的位置(bind),接下就能通信了吗? 不能,还需要启用,激活它,就像家里弄了网络面板,网线,还需要网络公司给我们开通网络权限.
int listen(int sockfd,int backlog)
listen的其实是在socket创建一个监听队列,队列大小为backlog,每一个client请求连接后,就会加入到队列里,等待处理
第四步: accept,处理连接请求
int accept(int sockfd,struct sockaddr * addr ,socklen_t * addrlen);
上一步,可以接收连接,然后加入到队列里,但是还需要对这些队列里的内容进行处理.
accept的作用,从队列取出一个连接数据,然后创建一个socket.你可能会问,为什么又创建了一个socket,原来不是有一个吗?
首先我们要明确的知道一个概念,两个socket一在一起才能进行通信,或者说通信的两个电脑之间各有一个socket,不允许一socket对应多个socket.
第一个socket,我们的成为socket,我们称为server socket,它在同一时刻与一个client socket进行连接,然后接收到必要的信息之后.根据这些信息产生一个socket A.这个新的socket A已后只与这个client socket进行通信.
这个过程我们称为accept
server端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
const int port = 8989; //端口
char message[] ="Hello World!";
void error_handle(char *message) {
printf("error: %s\n",message);
}
int main(int argc, char *argv[])
{
//创建一个server 端的socket
// man 7 socket, man 2 socket
int serv_sock = socket(AF_INET,SOCK_STREAM,0);
if(serv_sock == -1) {
error_handle("socket() failed!");
return 1;
}
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
//清空
// man 7 ip
memset(&serv_addr,0,sizeof(serv_addr));
//赋值
serv_addr.sin_family = AF_INET;
//host to network short
//short 16位 2字节
serv_addr.sin_port = htons(port);
//host to network long 4字节
// INADDR_ANY man 7 ip
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 第二步: bind 端口与ip
//通用套接字地址结构体sockaddr
if( bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr) ) == -1) {
error_handle("bind() failed!");
return 1;
}
//第三步: 激活监听
if( listen(serv_sock,5) == -1) {
error_handle("listen() failed!");
return 1;
}
//第四步: 等待连接
socklen_t client_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock,(struct sockaddr *) &clnt_addr,&client_addr_size);
if(clnt_sock == -1)
{
error_handle("accept() error!");
return 1;
}
write(clnt_sock,message,sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
return EXIT_SUCCESS;
}
编译
gcc -g -o server 1-hello_world_server.c
测试
netcat -v 127.0.0.1 8989
结果
➞ netcat -v 127.0.0.1 8989
localhost [127.0.0.1] 8989 (sunwebadmins) open
Hello World!%
client端代码
client实现比较简单
- 创建socket
- 调用connect,与server socket进行连接
- 发送数据
- 接收返回的数据
- 结束client socket
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8989
void error_handle(char *message)
{
printf("error: %s\n", message);
}
int main()
{
// 创建一个client 端的socket
// man 7 socket, man 2 socket
int clnt_sock = socket(AF_INET, SOCK_STREAM, 0);
if (clnt_sock == -1)
{
error_handle("socket() failed!");
return 1;
}
// 连接到服务器
struct sockaddr_in clnt_addr;
memset(&clnt_addr, 0, sizeof(clnt_addr));
clnt_addr.sin_family = AF_INET;
//inet_addr man 7 ip
clnt_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
clnt_addr.sin_port = htons(SERVER_PORT);
if (connect(clnt_sock, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr)) == -1)
{
error_handle("connect() failed!");
return 1;
}
// 发送数据
// send(clnt_sock, message, sizeof(message), 0);
// 接收数据
char buffer[1024];
int str_len = read(clnt_sock, buffer, sizeof(buffer) - 1);
if (str_len == -1)
{
error_handle("read() failed!");
return 1;
}
else
{
buffer[str_len] = '\0';
printf("Message from server: %s\n", buffer);
}
close(clnt_sock);
return 0;
}
运行结果
➞ ./1-hello_world_client.out
Message from server: Hello World!
inet_addr 函数
inet_addr 是一个用于将 IPv4 地址从点分十进制字符串表示转换为网络字节序的 32 位二进制值的函数。它在 Linux 和其他类 Unix 系统的网络编程中广泛使用。
man 3 inet_addr
函数原型:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
参数:
cp:以点分十进制格式表示的 IPv4 地址字符串,例如 "192.168.0.1"。
返回值:
- 成功时,返回转换后的 32 位网络字节序的 IPv4 地址。
- 失败时,返回
INADDR_NONE(通常为 -1)。
inet_addr 函数将点分十进制格式的 IPv4 地址字符串转换为网络字节序的 32 位二进制值。它执行以下步骤:
- 将点分十进制的 IPv4 地址字符串按照点号分隔符分割为四个部分,每个部分表示一个字节。
- 将每个部分转换为对应的整数值(0-255)。
- 将四个字节组合成一个 32 位的二进制值,并按照网络字节序排列。
例如,对于 IP 地址 "192.168.0.1",inet_addr 函数将返回一个 32 位的整数值 0x0100A8C0(网络字节序)。
使用示例:
#include <stdio.h>
#include <arpa/inet.h>
int main() {
const char *ip_str = "192.168.0.1";
in_addr_t ip_addr = inet_addr(ip_str);
if (ip_addr == INADDR_NONE) {
printf("无效的 IP 地址\n");
} else {
printf("IP 地址的网络字节序表示: 0x%08X\n", ip_addr);
}
return 0;
}
输出:
IP 地址的网络字节序表示: 0x0100A8C0
需要注意的是,inet_addr 函数只支持 IPv4 地址的转换,对于 IPv6 地址,需要使用 inet_pton 函数进行转换。
另外,由于 inet_addr 函数不能处理格式错误的 IP 地址字符串,因此在实际使用中,通常推荐使用更加健壮和通用的 inet_pton 函数来进行 IP 地址的转换。
linux的文件操作
每一个程序都有多个文件描述符,从 0 到 1023,这些0,1,2文件描述符被称为标准输入、标准输出、标准错误。
这个文件描述符是由linux系统维护的.
int open(const char *pathname, int flags);
int close(int fd);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
socket类型与协议设置
下我只与我理解的内容,如果全部都写,那么就变在了抄书了.
int socket(int domain, int type, int protocol);
其中的domain参数指定协议族,常见的有AF_INET, AF_INET6, AF_UNIX等,分别对应IPv4, IPv6, Unix Domain Socket.
type参数指定socket类型,常见的有SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_SEQPACKET等,分别对应TCP流式socket, UDP数据报式socket, 原始套接字, 顺序包套接字等.
protocol参数指定协议,常见的有IPPROTO_TCP, IPPROTO_UDP, IPPROTO_RAW, IPPROTO_SCTP等,分别对应TCP, UDP, 原始协议, SCTP等.
一般情况下,我们只需要设置domain和type参数,因为protocol参数一般由系统自动设置.
例如,创建一个TCP流式socket,可以使用如下代码:
这个用到的最多,其实也只用到这个(我们自己写的简单代码).
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
创建一个UDP数据报式socket,可以使用如下代码:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
创建一个原始套接字,可以使用如下代码:
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
传输的数据不存在数据边界
书上告诉我们,socket数据的传输是无边界的,也就是说,我们发送的数据可能被拆分成多个包,也可能被粘包,接收方也可能一次接收多个包,这取决于网络的传输速率和接收方处理能力.
这里给出一个我根据书上写的client.c代码,用来接收TCP流式socket数据:
它一次只能读取一个字节.
int tot = 0;
int read_len;
char buf[1024]
while(1) {
//一次读取一个字节
read_len = read(sockfd, buf, 1);
if( read_len == -1) break;
//输出读取的字符
printf("%c",buf[0]);
tot += read_len;
}
地址族与数据序列
书上讲解了一些ipv4地址的概念,以及ipv6地址的概念。
又说了一些端口号的概念,以及端口号的作用。
但是还是很复杂的,直接理解我第一章节说的内容,每个概念只有一句话.但是这是建立在我对网络编程的了解基础上的.
下讲解了
mindmap
root((sockadd_in))
sin_family(sin_family)
AF_INET(AF_INET)
AF_INET6(AF_INET6)
AF_Local(AF_Local)
sin_port(sin_port)
htons("htons(int)")
sin_addr(sin_addr)
s_addr(s_addr)
inet_addr("inet_addr(char*)")
需要把sockaddr_in转化成struct sockaddr *才能使用bind()等函数.
网络字节序与地址的转换
htons(int):将int类型整数转换为网络字节序。htonl(int):将int类型整数转换为网络字节序。ntohs(int):将int类型整数转换为主机字节序。ntohl(int):将int类型整数转换为主机字节序。inet_addr(char*):将char*类型字符串转换为in_addr类型。inet_aton(char*, struct in_addr*):将char*类型字符串转换为in_addr类型。inet_ntoa(struct in_addr):将in_addr类型转换为char*类型字符串,失败返回-1
每次创建服务器端socket都要输入IP地址会有些繁琐,此时可以使用如下的初始化地址信息
struct sockaddr_in server_addr;
char * serv_port = "8989";
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(serv_port));
server_addr.sin_addr.s_addr = inet_addr(INADDR_ANY);
基于tcp的服务器端/客户端
详细的内容看这里,我就不复制了:
https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/README.md
TCP/IP 协议栈共分为 4 层,可以理解为数据收发分成了 4 个层次化过程,通过层次化的方式来解决问题
- 链路层: 物理层,负责网络设备之间的通信,如网卡、网线等
- IP层: 数据包进行中线选择
- TCP/UDP层: 负责数据包的传输,保证数据包的可靠性和顺序性
实现基于 TCP 的服务器/客户端
代码:
hello_server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
//调用 socket 函数创建套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//调用 bind 函数分配ip地址和端口号
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
//调用 listen 函数将套接字转为可接受连接状态
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
//调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
error_handling("accept() error");
//稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
代码: hello_client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
//创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字
//如果调用 connect 函数,将成为客户端套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//调用 connect 函数向服务器发送连接请求
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1)
error_handling("read() error!");
printf("Message from server : %s \n", message);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
echo服务器/客户端
程序运行的基本方式:
- 服务器端在同一时刻只与一个客户端相连,并提供回声服务。
- 服务器端依次向 5 个客户端提供服务并退出。
for循环5次 - 客户端接受用户输入的字符串并发送到服务器端。
- 服务器端将接受的字符串数据传回客户端,即「回声」
- 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_adr_sz = sizeof(clnt_adr);
//调用 5 次 accept 函数,共为 5 个客户端提供服务
for (i = 0; i < 5; i++)
{
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
if (clnt_sock == -1)
error_handling("accept() error");
else
printf("Connect client %d \n", i + 1);
while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error!");
else
puts("Connected...........");
while (1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
FAQ
问: 我在在linux下使用socket进行tcp网络编程,如果client端调用了close(sock)函数,那么server端口的read函数会读取到什么?
答:
在Linux下使用socket进行TCP网络编程时,如果客户端调用了close(sock)函数,服务器端的read函数的行为取决于客户端关闭连接的方式和时机。
通常情况下,当客户端调用close(sock)函数时,会向服务器发送一个FIN包,表示客户端想要关闭连接。服务器收到FIN包后,会做出以下响应:
-
如果服务器已经读取了所有客户端发送的数据,那么
read函数会返回0,表示已经到达文件末尾(EOF)。这意味着客户端已经关闭了连接,并且没有更多的数据可读。 -
如果服务器还没有读取完客户端发送的所有数据,那么
read函数会继续读取剩余的数据,直到读取完所有数据或者遇到错误为止。在这种情况下,read函数会返回实际读取到的字节数。当所有数据读取完毕后,再次调用read函数会返回0,表示已经到达文件末尾(EOF)。 -
如果客户端在发送数据的过程中突然关闭了连接,那么服务器的
read函数可能会返回一个错误,通常是ECONNRESET错误,表示连接被重置。
需要注意的是,如果客户端在关闭连接之前没有发送所有的数据,那么服务器可能会丢失一部分数据。为了确保数据的完整性,通常建议在关闭连接之前,先调用shutdown(sock, SHUT_WR)函数,告诉操作系统不再发送数据,然后再调用close(sock)函数关闭连接。
总之,服务器端的read函数在客户端关闭连接时的行为取决于具体的情况:
- 如果客户端发送了所有数据并关闭连接,
read函数会返回0,表示EOF。 - 如果客户端关闭连接时还有未读取的数据,
read函数会继续读取剩余数据,直到读取完毕,然后返回0,表示EOF。 - 如果客户端异常关闭连接,
read函数可能会返回错误,如ECONNRESET。
作为服务器端,需要正确处理这些不同的情况,以确保程序的正确性和鲁棒性。
问: 如何检测到 read函数返回一个错误, 例如是ECONNRESET?
在Linux系统中,当read函数返回错误时,会设置全局变量errno来指示具体的错误类型。你可以通过检查errno的值来判断是否发生了ECONNRESET错误。
下面是一个示例代码,演示了如何检测read函数返回的错误:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
ssize_t read_data(int sockfd, void *buf, size_t len) {
ssize_t num_read = read(sockfd, buf, len);
if (num_read == -1) {
if (errno == ECONNRESET) {
printf("Connection reset by peer\n");
} else {
perror("read");
}
return -1;
}
return num_read;
}
在上面的代码中,read_data函数封装了read函数的调用。它检查read函数的返回值:
- 如果返回值为-1,表示发生了错误。此时,通过检查
errno的值来判断具体的错误类型:- 如果
errno的值等于ECONNRESET,表示连接被对端重置,可以输出相应的错误信息。 - 否则,可以使用
perror函数输出错误信息,perror函数会根据errno的值自动生成对应的错误描述。
- 如果
- 如果返回值不为-1,表示读取成功,函数返回实际读取到的字节数。
在实际使用时,你可以根据需要对read_data函数进行调用,并根据返回值进行相应的处理:
char buffer[1024];
ssize_t num_read = read_data(sockfd, buffer, sizeof(buffer));
if (num_read == -1) {
// 处理错误情况
} else if (num_read == 0) {
// 处理连接关闭的情况
} else {
// 处理成功读取数据的情况
}
通过检查read_data函数的返回值,你可以判断是否发生了错误,并根据具体的错误类型采取相应的处理措施。
总之,通过检查read函数的返回值和errno的值,你可以检测并处理read函数返回的错误,包括ECONNRESET错误。这样可以增强程序的健壮性和可靠性。
基于tcp的服务器端/客户端(2)
本章节详细的讲解了TCP 中必要的理论知识
回声客户端问题的解决办法 这个问题其实很容易解决,因为可以提前接受数据的大小。若之前传输了20字节长的字符串,则再接收时循环调用 read 函数读取 20 个字节即可。既然有了解决办法,核心那么代码如下:
//得到发送的数据大小
str_len = write(sockfd,message,sizeof(message));
int recv_len = 0;
while( recv_len < str_len ) {
int recv_cnt += read(sockfd,recv_buf,str_len-recv_len);
//对方关闭了连接
if( recv_cnt == 0 ) {
break;
}
if( recv_cnt < 0 ) {
error_handing("read error")!
}
recv_len += recv_cnt;
}
message[recv_len] = '\0';
printf("Received message: %s\n",message);
TCP 原理
-
TCP socket中的I/O缓冲
-
IO缓冲在每个TCP socket中单独存在.
-
IO缓冲在在创建TCP socket自动生成.
-
即使关闭socket 也会继续传递输出缓冲中遗留的数据.
-
关闭socket会丢失输入缓冲中的数据.
不会发生超过输入缓冲大小的数据传输!
write函数会在数据移到输出缓冲时返回.
1. TCP连接
3次握手(three-way handshaking)
[SYN] SEQ:1000 ACK:NULL
[SYN+ACK] SEQ: 2000, ACK: 1001
[ACK] SEQ: 1001, ACK: 2001
2. TCP传输
3. TCP断开
1 单方面断开连接带来的问题
主机 A 发送完最后的数据后,调用 close 函数断开了最后的连接,之后主机 A 无法再接受主机 B 传输的数据。实际上,是完全无法调用与接受数据相关的函数。最终,由主机 B 传输的、主机 A 必须要接受的数据也销毁了。
2. shutdown函数的作用
#include <sys/socket.h>
int shutdown(int sock, int howto);
/*
成功时返回 0 ,失败时返回 -1
sock: 需要断开套接字文件描述符
howto: 传递断开方式信息
*/
调用上述函数时,第二个参数决定断开连接的方式,其值如下所示:
SHUT_RD: 断开输入流SHUT_WR: 断开输出流SHUT_RDWR: 同时断开 I/O 流
若向 shutdown 的第二个参数传递SHUT_RD,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用相关函数。如果向 shutdown 的第二个参数传递SHUT_WR,则中断输出流,也就无法传输数据。若如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字SHUT_RDWR,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以SHUT_RD为参数,另一次以SHUT_WR为参数
3. 为什么要半关闭连接
留下足够的时间,等所有的数据交换完不就行了,好像也没有必要使用半关闭.
考虑以下情况:
一旦客户端连接到服务器,服务器将约定的文件传输给客户端,客户端收到后发送字符串「Thank you」给服务器端。
此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序的还嫌难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止的调用输入函数,因为这有可能导致程序阻塞。
是否可以让服务器和客户端约定一个代表文件尾的字符?
这种方式也有问题,因为这意味这文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接受 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF ?
断开输出流时向主机传输 EOF。
当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。
4. 基于半关闭的文件传输程序

server代码
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sd, clnt_sd;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr, clnt_adr;
//1.创建 server_socket
serv_sd = socket(PF_INET, SOCK_STREAM, 0);
//2. 设定地址
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(8899);
//3 进行bind
bind(serv_sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
//4 监听
listen(serv_sd, 5);
socklen_t clnt_adr_sz = sizeof(clnt_adr);
clnt_sd = accept(serv_sd, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
FILE *fp = fopen("file_server.c", "rb");
while (1)
{
int read_cnt = fread(buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE)
{
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
//文件发送完毕
shutdown(clnt_sd, SHUT_WR); //半关闭,写入
read(clnt_sd, buf, BUF_SIZE); //等待读取
printf("Received message from client: %s\n", buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
return 0;
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
client代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
fp = fopen("receive.cpp", "wb");
sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
while ((read_cnt = read(sd, buf, BUF_SIZE)) != 0)
fwrite((void *)buf, 1, read_cnt, fp);
puts("Received file data");
write(sd, "Thank you", 10);
fclose(fp);
close(sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
第 8 章 域名及网络地址
本章代码,在TCP-IP-NetworkNote中可以找到。
8.1 域名系统
DNS 是对IP地址和域名进行相互转换的系统,其核心是 DNS 服务器
8.1.1 什么是域名
域名就是我们常常在地址栏里面输入的地址,将比较难记忆的IP地址变成人类容易理解的信息。
8.1.2 DNS 服务器
相当于一个字典,可以查询出某一个域名对应的IP地址

如图所示,显示了 DNS 服务器的查询路径。
8.2 IP地址和域名之间的转换
8.2.1 程序中有必要使用域名吗?
一句话,需要,因为IP地址可能经常改变,而且也不容易记忆,通过域名可以随时更改解析,达到更换IP的目的
8.2.2 利用域名获取IP地址
使用以下函数可以通过传递字符串格式的域名获取IP地址
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
/*
成功时返回 hostent 结构体地址,失败时返回 NULL 指针
*/
这个函数使用方便,只要传递字符串,就可以返回域名对应的IP地址。只是返回时,地址信息装入 hostent 结构体。此结构体的定义如下:
struct hostent
{
char *h_name; /* Official name of host. */
char **h_aliases; /* Alias list. */
int h_addrtype; /* Host address type. */
int h_length; /* Length of address. */
char **h_addr_list; /* List of addresses from name server. */
};
从上述结构体可以看出,不止返回IP信息,同事还带着其他信息一起返回。域名转换成IP时只需要关注 h_addr_list 。下面简要说明上述结构体的成员:
- h_name:该变量中存有官方域名(Official domain name)。官方域名代表某一主页,但实际上,一些著名公司的域名并没有用官方域名注册。
- h_aliases:可以通过多个域名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可以指定其他域名。这些信息可以通过 h_aliases 获得。
- h_addrtype:gethostbyname 函数不仅支持 IPV4 还支持 IPV6 。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPV4 ,则此变量中存有 AF_INET。
- h_length:保存IP地址长度。若是 IPV4 地址,因为是 4 个字节,则保存4;IPV6 时,因为是 16 个字节,故保存 16
- h_addr_list:这个是最重要的的成员。通过此变量以整数形式保存域名相对应的IP地址。另外,用户比较多的网站有可能分配多个IP地址给同一个域名,利用多个服务器做负载均衡,。此时可以通过此变量获取IP地址信息。
调用 gethostbyname 函数后,返回的结构体变量如图所示:

下面的代码通过一个例子来演示 gethostbyname 的应用,并说明 hostent 结构体变量特性。
编译运行:
gcc gethostbyname.c -o hostname
./hostname www.baidu.com
结果:

如图所示,显示出了对百度的域名解析
可以看出,百度有一个域名解析是 CNAME 解析的,指向了shifen.com,关于百度具体的解析过程。
这一部分牵扯到了很多关于DNS解析的过程,还有 Linux 下关于域名解析的一些命令,我找了一部分资料,可以点下面的链接查看比较详细的:
仔细阅读这一段代码:
inet_ntoa(*(struct in_addr *)host->h_addr_list[i])
若只看 hostent 的定义,结构体成员 h_addr_list 指向字符串指针数组(由多个字符串地址构成的数组)。但是字符串指针数组保存的元素实际指向的是 in_addr 结构体变量中地址值而非字符串,也就是说(struct in_addr *)host->h_addr_list[i]其实是一个指针,然后用*符号取具体的值。如图所示:

8.2.3 利用IP地址获取域名
请看下面的函数定义:
#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);
/*
成功时返回 hostent 结构体变量地址值,失败时返回 NULL 指针
addr: 含有IP地址信息的 in_addr 结构体指针。为了同时传递 IPV4 地址之外的全部信息,该变量的类型声明为 char 指针
len: 向第一个参数传递的地址信息的字节数,IPV4时为 4 ,IPV6 时为16.
family: 传递地址族信息,ipv4 是 AF_INET ,IPV6是 AF_INET6
*/
下面的代码演示使用方法:
编译运行:
gcc gethostbyaddr.c -o hostaddr
./hostaddr 8.8.8.8
结果:

从图上可以看出,8.8.8.8这个IP地址是谷歌的。
8.3 基于 Windows 的实现
暂略
8.4 习题
以下答案仅代表本人个人观点,可能不是正确答案。
-
下列关于DNS的说法错误的是?
答:字体加粗的表示正确答案。
- 因为DNS从存在,故可以使用域名代替IP
- DNS服务器实际上是路由器,因为路由器根据域名决定数据的路径
- 所有域名信息并非集中与 1 台 DNS 服务器,但可以获取某一 DNS 服务器中未注册的所有地址
- DNS 服务器根据操作系统进行区分,Windows 下的 DNS 服务器和 Linux 下的 DNS 服务器是不同的。
-
阅读如下对话,并说明东秀的方案是否可行。(因为对话的字太多,用图代替)

答:答案就是可行,DNS 服务器是分布式的,一台坏了可以找其他的。
-
在浏览器地址输入 www.orentec.co.kr ,并整理出主页显示过程。假设浏览器访问默认 DNS 服务器中并没有关于 www.orentec.co.kr 的地址信息.
答:可以参考一下知乎回答,在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?,我用我自己的理解,简单说一下,首先会去向上一级的 DNS 服务器去查询,通过这种方式逐级向上传递信息,一直到达根服务器时,它知道应该向哪个 DNS 服务器发起询问。向下传递解析请求,得到IP地址候原路返回,最后会将解析的IP地址传递到发起请求的主机。
代码
gethostbyaddr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int i;
struct hostent *host;
struct sockaddr_in addr;
if (argc != 2)
{
printf("Usage : %s <IP>\n", argv[0]);
exit(1);
}
memset(&addr, 0, sizeof(addr));
addr.sin_addr.s_addr = inet_addr(argv[1]);
host = gethostbyaddr((char *)&addr.sin_addr, 4, AF_INET);
if (!host)
error_handling("gethost... error");
printf("Official name: %s \n", host->h_name);
for (i = 0; host->h_aliases[i]; i++)
printf("Aliases %d:%s \n", i + 1, host->h_aliases[i]);
printf("Address type: %s \n",
(host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
for (i = 0; host->h_addr_list[i]; i++)
printf("IP addr %d: %s \n", i + 1,
inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
gethostbyname.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int i;
struct hostent *host;
if (argc != 2)
{
printf("Usage : %s <addr>\n", argv[0]);
exit(1);
}
// 把参数传递给函数,返回结构体
host = gethostbyname(argv[1]);
if (!host)
error_handling("gethost... error");
// 输出官方域名
printf("Official name: %s \n", host->h_name);
// Aliases 貌似是解析的 cname 域名?
for (i = 0; host->h_aliases[i]; i++)
printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]);
//看看是不是ipv4
printf("Address type: %s \n",
(host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
// 输出ip地址信息
for (i = 0; host->h_addr_list[i]; i++)
printf("IP addr %d: %s \n", i + 1,
inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
第 9 章 套接字的多种可选项
本章代码,在TCP-IP-NetworkNote中可以找到。
9.1 套接字可选项和 I/O 缓冲大小
我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解这些特性并根据实际需要进行更改也很重要
9.1.1 套接字多种可选项
我们之前写得程序都是创建好套接字之后直接使用的,此时通过默认的套接字特性进行数据通信,这里列出了一些套接字可选项。
| 协议层 | 选项名 | 读取 | 设置 |
|---|---|---|---|
| SOL_SOCKET | SO_SNDBUF | O | O |
| SOL_SOCKET | SO_RCVBUF | O | O |
| SOL_SOCKET | SO_REUSEADDR | O | O |
| SOL_SOCKET | SO_KEEPALIVE | O | O |
| SOL_SOCKET | SO_BROADCAST | O | O |
| SOL_SOCKET | SO_DONTROUTE | O | O |
| SOL_SOCKET | SO_OOBINLINE | O | O |
| SOL_SOCKET | SO_ERROR | O | X |
| SOL_SOCKET | SO_TYPE | O | X |
| IPPROTO_IP | IP_TOS | O | O |
| IPPROTO_IP | IP_TTL | O | O |
| IPPROTO_IP | IP_MULTICAST_TTL | O | O |
| IPPROTO_IP | IP_MULTICAST_LOOP | O | O |
| IPPROTO_IP | IP_MULTICAST_IF | O | O |
| IPPROTO_TCP | TCP_KEEPALIVE | O | O |
| IPPROTO_TCP | TCP_NODELAY | O | O |
| IPPROTO_TCP | TCP_MAXSEG | O | O |
从表中可以看出,套接字可选项是分层的。
-
IPPROTO_IP 可选项是IP协议相关事项
-
IPPROTO_TCP 层可选项是 TCP 协议的相关事项
-
SOL_SOCKET 层是套接字的通用可选项。
9.1.2 getsockopt & setsockopt
可选项的读取和设置通过以下两个函数来完成
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
/*
成功时返回 0 ,失败时返回 -1
sock: 用于查看选项套接字文件描述符
level: 要查看的可选项协议层
optname: 要查看的可选项名
optval: 保存查看结果的缓冲地址值
optlen: 向第四个参数传递的缓冲大小。调用函数候,该变量中保存通过第四个参数返回的可选项信息的字节数。
*/
上述函数可以用来读取套接字可选项,下面的函数可以更改可选项:d
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
/*
成功时返回 0 ,失败时返回 -1
sock: 用于更改选项套接字文件描述符
level: 要更改的可选项协议层
optname: 要更改的可选项名
optval: 保存更改结果的缓冲地址值
optlen: 向第四个参数传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。
*/
下面的代码可以看出 getsockopt 的使用方法。下面示例用协议层为 SOL_SOCKET 、名为 SO_TYPE 的可选项查看套接字类型(TCP 和 UDP )。
编译运行:
gcc sock_type.c -o sock_type
./sock_type
结果:
SOCK_STREAM: 1
SOCK_DGRAM: 2
Socket type one: 1
Socket type two: 2
首先创建了一个 TCP 套接字和一个 UDP 套接字。然后通过调用 getsockopt 函数来获得当前套接字的状态。
用于验证套接类型的 SO_TYPE 是只读可选项,因为套接字类型只能在创建时决定,以后不能再更改。
9.1.3 SO_SNDBUF & SO_RCVBUF
创建套接字的同时会生成 I/O 缓冲。关于 I/O 缓冲,可以去看第五章。
SO_RCVBUF 是输入缓冲大小相关可选项,SO_SNDBUF 是输出缓冲大小相关可选项。用这 2 个可选项既可以读取当前 I/O 大小,也可以进行更改。通过下列示例读取创建套接字时默认的 I/O 缓冲大小。
编译运行:
gcc get_buf.c -o getbuf
./getbuf
运行结果:
Input buffer size: 87380
Output buffer size: 16384
可以看出本机的输入缓冲和输出缓冲大小。
下面的代码演示了,通过程序设置 I/O 缓冲区的大小
编译运行:
gcc get_buf.c -o setbuf
./setbuf
结果:
Input buffer size: 6144
Output buffer size: 6144
输出结果和我们预想的不是很相同,缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行。
9.2 SO_REUSEADDR
9.2.1 发生地址分配错误(Binding Error)
在学习 SO_REUSEADDR 可选项之前,应该好好理解 Time-wait 状态。看以下代码的示例:
这是一个回声服务器的服务端代码,可以配合第四章的 echo_client.c 使用,在这个代码中,客户端通知服务器终止程序。在客户端控制台输入 Q 可以结束程序,向服务器发送 FIN 消息并经过四次握手过程。当然,输入 CTRL+C 也会向服务器传递 FIN 信息。强制终止程序时,由操作系统关闭文件套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。
这样看不到是什么特殊现象,考虑以下情况:
服务器端和客户端都已经建立连接的状态下,向服务器控制台输入 CTRL+C ,强制关闭服务端
如果用这种方式终止程序,如果用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。
9.2.2 Time-wait 状态
观察以下过程:

假设图中主机 A 是服务器,因为是主机 A 向 B 发送 FIN 消息,故可想象成服务器端在控制台中输入 CTRL+C 。但是问题是,套接字经过四次握手后并没有立即消除,而是要经过一段时间的 Time-wait 状态。当然,只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind 函数调用过程中会发生错误。
实际上,不论是服务端还是客户端,都要经过一段时间的 Time-wait 过程。先断开连接的套接字必然会经过 Time-wait 过程,但是由于客户端套接字的端口是任意指定的,所以无需过多关注 Time-wait 状态。
那到底为什么会有 Time-wait 状态呢,在图中假设,主机 A 向主机 B 传输 ACK 消息(SEQ 5001 , ACK 7502 )后立刻消除套接字。但是最后这条 ACK 消息在传递过程中丢失,没有传递主机 B ,这时主机 B 就会试图重传。但是此时主机 A 已经是完全终止状态,因此主机 B 永远无法收到从主机 A 最后传来的 ACK 消息。基于这些问题的考虑,所以要设计 Time-wait 状态。
9.2.3 地址再分配
Time-wait 状态看似重要,但是不一定讨人喜欢。如果系统发生故障紧急停止,这时需要尽快重启服务起以提供服务,但因处于 Time-wait 状态而必须等待几分钟。因此,Time-wait 并非只有优点,这些情况下容易引发大问题。下图中展示了四次握手时不得不延长 Time-wait 过程的情况。

从图上可以看出,在主机 A 四次握手的过程中,如果最后的数据丢失,则主机 B 会认为主机 A 未能收到自己发送的 FIN 信息,因此重传。这时,收到的 FIN 消息的主机 A 将重启 Time-wait 计时器。因此,如果网络状况不理想, Time-wait 将持续。
解决方案就是在套接字的可选项中更改 SO_REUSEADDR 的状态。适当调整该参数,可将 Time-wait 状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR 的默认值为 0.这就意味着无法分配 Time-wait 状态下的套接字端口号。因此需要将这个值改成 1 。具体作法已在示例 reuseadr_eserver.c 给出,只需要把注释掉的东西解除注释即可。
optlen = sizeof(option);
option = TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);
此时,已经解决了上述问题。
9.3 TCP_NODELAY
9.3.1 Nagle 算法
为了防止因数据包过多而发生网络过载,Nagle 算法诞生了。它应用于 TCP 层。它是否使用会导致如图所示的差异:

图中展示了通过 Nagle 算法发送字符串 Nagle 和未使用 Nagle 算法的差别。可以得到一个结论。
只有接收到前一数据的 ACK 消息, Nagle 算法才发送下一数据。
TCP 套接字默认使用 Nagle 算法交换数据,因此最大限度的进行缓冲,直到收到 ACK 。左图也就是说一共传递 4 个数据包以传输一个字符串。从右图可以看出,发送数据包一共使用了 10 个数据包。由此可知,不使用 Nagle 算法将对网络流量产生负面影响。即使只传输一个字节的数据,其头信息都可能是几十个字节。因此,为了提高网络传输效率,必须使用 Nagle 算法。
Nagle 算法并不是什么情况下都适用,网络流量未受太大影响时,不使用 Nagle 算法要比使用它时传输速度快。最典型的就是「传输大文数据」。将文件数据传入输出缓冲不会花太多时间,因此,不使用 Nagle 算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而在无需等待 ACK 的前提下连续传输,因此可以大大提高传输速度。
所以,未准确判断数据性质时不应禁用 Nagle 算法。
9.3.2 禁用 Nagle 算法
禁用 Nagle 算法应该使用:
int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));
通过 TCP_NODELAY 的值来查看Nagle 算法的设置状态。
opt_len = sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len);
如果正在使用Nagle 算法,那么 opt_val 值为 0,如果禁用则为 1.
关于这个算法,可以参考这个回答:TCP连接中启用和禁用TCP_NODELAY有什么影响?
9.4 基于 Windows 的实现
暂略
9.5 习题
以下答案仅代表本人个人观点,可能不是正确答案。
-
下列关于 Time-wait 状态的说法错误的是?
答:以下字体加粗的代表正确。
- Time-wait 状态只在服务器的套接字中发生
- 断开连接的四次握手过程中,先传输 FIN 消息的套接字将进入 Time-wait 状态。
- Time-wait 状态与断开连接的过程无关,而与请求连接过程中 SYN 消息的传输顺序有关
- Time-wait 状态通常并非必要,应尽可能通过更改套接字可选项来防止其发生
-
TCP_NODELAY 可选项与 Nagle 算法有关,可通过它禁用 Nagle 算法。请问何时应考虑禁用 Nagle 算法?结合收发数据的特性给出说明。
答:当网络流量未受太大影响时,不使用 Nagle 算法要比使用它时传输速度快,比如说在传输大文件时。
代码
get_buf.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
int snd_buf, rcv_buf, state;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
if (state)
error_handling("getsockopt() error");
len = sizeof(rcv_buf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
if (state)
error_handling("getsockopt() error");
printf("Input buffer size: %d \n", rcv_buf);
printf("Output buffer size: %d \n", snd_buf);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
set_buf
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
int snd_buf = 1024 * 3, rcv_buf = 1024 * 3;
int state;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(snd_buf);
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
if (state)
error_handling("setsockopt() error");
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
if (state)
error_handling("setsockopt() error");
len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
if (state)
error_handling("getsockopt() error");
len = sizeof(rcv_buf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
if (state)
error_handling("getsockopt() error");
printf("Input buffer size: %d \n", rcv_buf);
printf("Output buffer size: %d \n", snd_buf);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
reuseadr_eserver
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
#define TRUE 1
#define FALSE 0
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[30];
int option, str_len;
socklen_t optlen, clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
/*
optlen = sizeof(option);
option = TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);
*/
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)))
error_handling("bind() error");
if (listen(serv_sock, 5) == -1)
error_handling("listen error");
clnt_adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
while ((str_len = read(clnt_sock, message, sizeof(message))) != 0)
{
write(clnt_sock, message, str_len);
write(1, message, str_len);
}
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
sock_type
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int tcp_sock, udp_sock;
int sock_type;
socklen_t optlen;
int state;
optlen = sizeof(sock_type);
tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
printf("SOCK_STREAM: %d\n", SOCK_STREAM);
printf("SOCK_DGRAM: %d\n", SOCK_DGRAM);
state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
if (state)
error_handling("getsockopt() error");
printf("Socket type one: %d \n", sock_type);
state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
if (state)
error_handling("getsockopt() error");
printf("Socket type two: %d \n", sock_type);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
为了加大服务器同时处理用户的连接,有效的利用CPU
- 多进程服务器: 创建多个进程提供服务
- 多路复用: 捆绑并统一管理IO对象提供服务
- 多线程: 生成与客户短等量化的线程提供服务
10.1 进程概念及引用
理解进程
进程的定义如下:占用内存空间的正在运行的程序
进程ID
在说进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程ID」,其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1 。接下来观察在 Linux 中运行的进程。
fork创建进程
创建进程的方式很多,此处只介绍用于创建多进程服务端的 fork 函数。
#include <unistd.h>
pid_t fork(void);
// 成功时返回进程ID,失败时返回 -1
fork 函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用 fork 函数的进程。另外,两个进程都执行 fork 函数调用后的语句(准确的说是在 fork 函数返回后)。但因为是通过同一个进程、复制相同的内存空间,之后的程序流要根据 fork 函数的返回值加以区分。即利用 fork 函数的如下特点区分程序执行流程。
- 父进程:fork 函数返回子进程 ID
- 子进程:fork 函数返回 0
此处,「父进程」(Parent Process)指原进程,即调用 fork 函数的主体,而「子进程」(Child Process)是通过父进程调用 fork 函数复制出的进程。接下来是调用 fork 函数后的程序运行流程。如图所示:
从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25,因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回类型区分父子进程。父进程的 lval 的值增加 1 ,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval 。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子:
#include <stdio.h>
#include <unistd.h>
int gval = 10;
int main(int argc, char *argv[])
{
pid_t pid;
int lval = 20;
gval++, lval += 5;
pid = fork();
if (pid == 0)
gval += 2, lval += 2;
else
gval -= 2, lval -= 2;
if (pid == 0)
printf("Child Proc: [%d,%d] \n", gval, lval);
else
printf("Parent Proc: [%d,%d] \n", gval, lval);
return 0;
}
10.2 进程与僵尸进程
文件操作中,关闭文件和打开文件同等重要。同样,进程销毁和进程创建也同等重要。如果未认真对待进程销毁,他们将变成僵尸进程。
进程的工作完成后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作「僵尸进程」,这也是给系统带来负担的原因之一。
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源
UNIX命令ps列出的进程的状态("STAT")栏标示为 "Z"则为僵尸进程。[1]
收割僵尸进程的方法是通过kill命令手工向其父进程发送SIGCHLD信号。如果其父进程仍然拒绝收割僵尸进程,则终止父进程,使得init进程收养僵尸进程。init进程周期执行wait系统调用收割其收养的所有僵尸进程。
产生僵尸进程的原因
产生僵尸进程的原因是: 当子进程结束时,操作系统不会主动将子进程的退出状态传递给父进程 。只有父进程主动请求获取子进程的退出状态, 操作系统才会将其传递。如果父进程没有主动要求获取子进程的退出状态,操作系统将一直保存子进程的退出状态,导致子进程长时间处于僵尸进程状态。因此,为了及时回收子进程,父进程应该主动获取子进程的退出状态。
zombie.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid = fork();
if (pid == 0)
{
puts("Hi, I am a child Process");
}
else
{
printf("Child Process ID: %d \n", pid);
sleep(30);
// 因为暂停了 30 秒,所以在这个时间内可以验证一下子进程是否为僵尸进程。
}
if (pid == 0)
puts("End child proess");
else
puts("End parent process");
return 0;
}

销毁僵尸 wait函数
父进程应该主动请求获取子进程的返回值以销毁子进程。使用 wait 函数来获取子进程的终止状态,通过宏 WIFEXITED 和 WEXITSTATUS 来分离和处理子进程的返回值。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h>
int main(int argc, char *argv[]) { int status; pid_t pid = fork(); //这里的子进程将在第13行通过 return 语句终止
if (pid == 0)
{
return 3;
}
else
{
printf("Child PID: %d \n", pid);
pid = fork(); //这里的子进程将在 21 行通过 exit() 函数终止
if (pid == 0)
{
exit(7);
}
else
{
printf("Child PID: %d \n", pid);
wait(&status); //之间终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁
if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值
printf("Child send one: %d \n", WEXITSTATUS(status));
wait(&status); //因为之前创建了两个进程,所以再次调用 wait 函数和宏
if (WIFEXITED(status))
printf("Child send two: %d \n", WEXITSTATUS(status));
sleep(30);
}
}
return 0;
}
销毁僵尸 waitpid函数
wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。这是防止僵尸进程的第二种方法,也是防止阻塞的方法。
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
/*
成功时返回终止的子进程ID 或 0 ,失败时返回 -1
pid: 等待终止的目标子进程的ID,若传 -1,则与 wait 函数相同,可以等待任意子进程终止
statloc: 与 wait 函数的 statloc 参数具有相同含义
options: 传递头文件 sys/wait.h 声明的常量 WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。
*/
waitpid.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork();
if (pid == 0)
{
sleep(15); //用 sleep 推迟子进程的执行
return 24;
}
else
{
//调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0
while (!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3 sec.");
}
if (WIFEXITED(status))
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
10.3 信号处理
子进程究竟何时终止?调用 waitpid 函数后要无休止的等待吗?
信号是一种软中断,它是由操作系统向进程发送的通知消息。信号的作用是通知进程发生了某种事件,如进程终止、进程收到某种输入、进程执行时间到达某一点等。信号的处理方式是通过信号处理函数来实现的。
子进程终止的识别主体是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序
信号与signal函数
我们可以注册信号处理函数,当信号发生时,操作系统将调用相应的信号处理函数。信号处理函数的原型如下:
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
/*
为了在产生信号时调用,返回之前注册的函数指针
函数名: signal
参数:int signo,void(*func)(int)
返回类型:参数类型为int型,返回 void 型函数指针
*/
这个函数比较难,我这样理解
有函数为
void func_a(int)
这个函数的是一个返回值为void,参数为int的函数.
void (*func_b)(int)
//相当于
//定义一个类型,函数指针
typedef void (*)(int) func_a_pointer;
// 定义一个指向函数的变量
func_a_pointer func_a_p = func_a;
怎么理解函数指针

再难一点,定义函数的返回值 为函数指针
void (*func_c(int))(int)
typedef void (*)(int) func_a_pointer;
// 定义一个指向函数的变量
func_a_pointer function_c(int);
再难一点,函数的返回值为函数指针,且函数的参数还有一个函数指针
void (*signal(int signo, void (*func)(int)))(int);
调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。发生第一个参数代表的情况时,调用第二个参数所指的函数。下面给出可以在 signal 函数中注册的部分特殊情况和对应的函数。
- SIGALRM:已到通过调用 alarm 函数时候产生
- SIGINT:输入 ctrl+c
- SIGCHLD:子进程终止
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig) //信号处理器
{
if (sig == SIGALRM)
puts("Time out!");
alarm(2); //为了每隔 2 秒重复产生 SIGALRM 信号,在信号处理器中调用 alarm 函数
}
void keycontrol(int sig) //信号处理器
{
if (sig == SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char *argv[])
{
int i;
signal(SIGALRM, timeout); //注册信号及相应处理器
signal(SIGINT, keycontrol);
alarm(2); //预约 2 秒候发生 SIGALRM 信号
for (i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
发生信号时将唤醒由于调用 sleep 函数而进入阻塞状态的进程。
利用 sigaction 函数进行信号处理
还有一个函数,叫做 sigaction 函数,他类似于 signal 函数,而且可以完全代替后者,也更稳定。之所以稳定,是因为:
signal 函数在 Unix 系列的不同操作系统可能存在区别,但 sigaction 函数完全相同
实际上现在很少用 signal 函数编写程序,他只是为了保持对旧程序的兼容,下面介绍 sigaction 函数,只讲解可以替换 signal 函数的功能。
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
/*
成功时返回 0 ,失败时返回 -1
act: 对于第一个参数的信号处理函数(信号处理器)信息。
oldact: 通过此参数获取之前注册的信号处理函数指针,若不需要则传递 0
*/
声明并初始化 sigaction 结构体变量以调用上述函数,该结构体定义如下:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout; //保存函数指针
sigemptyset(&act.sa_mask); //将 sa_mask 成员的所有位初始化成0
act.sa_flags = 0; //sa_flags 同样初始化成 0
sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。
alarm(2); //2 秒后发生 SIGALRM 信号
for (int i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
10.3.4 利用信号处理技术消灭僵尸进程
下面利用子进程终止时产生 SIGCHLD 信号这一点,来用信号处理来消灭僵尸进程。看以下代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if (WIFEXITED(status))
{
printf("Removed proc id: %d \n", id); //子进程的 pid
printf("Child send: %d \n", WEXITSTATUS(status)); //子进程的返回值
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
pid = fork();
if (pid == 0) //子进程执行阶段
{
puts("Hi I'm child process");
sleep(10);
return 12;
}
else //父进程执行阶段
{
printf("Child proc id: %d\n", pid);
pid = fork();
if (pid == 0)
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for (i = 0; i < 5; i++)
{
puts("wait");
sleep(5);
}
}
}
return 0;
}
10.4 基于多任务的并发服务器
之前的回声服务器每次只能同事向 1 个客户端提供服务。因此,需要扩展回声服务器,使其可以同时向多个客户端提供服务。下图是基于多进程的回声服务器的模型。
从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。如果请求的客户端有 5 个,则将创建 5 个子进程来提供服务,为了完成这些任务,需要经过如下过程:
- 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求
- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
- 第三阶段:子进程利用传递来的文件描述符提供服务
Q ? 如何传递给子进程文件描述符
A 子进程会复制父进程的所有的资源,所以不需要传递Q: 子进程会复制父进程的server_sd(套接字文件描述符)吗? 那他会收到cient的数据吗?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc; //防止僵尸进程
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0); //注册信号处理器,把成功的返回值给 state
serv_sock = socket(PF_INET, SOCK_STREAM, 0); //创建服务端套接字
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) //分配IP地址和端口号
error_handling("bind() error");
if (listen(serv_sock, 5) == -1) //进入等待连接请求状态
error_handling("listen() error");
while (1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
else
puts("new client connected...");
pid = fork(); //此时,父子进程分别带有一个套接字
if (pid == -1)
{
close(clnt_sock);
continue;
}
if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务
{
close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程
while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock); //通过 accept 函数创建的套接字文件描述符已经复制给子进程,因为服务器端要销毁自己拥有的
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
10.4.2 实现并发服务器
10.4.3 通过 fork 函数复制文件描述符
示例中给出了通过 fork 函数复制文件描述符的过程。父进程将 2 个套接字(一个是服务端套接字另一个是客户端套接字)文件描述符复制给了子进程。
调用 fork 函数时赋值父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。
如图所示,1 个套接字存在 2 个文件描述符时,只有 2 个文件描述符都终止(销毁)后,才能销毁套接字。如果维持图中的状态,即使子进程销毁了与客户端连接的套接字文件描述符,也无法销毁套接字(服务器套接字同样如此)。因此调用 fork 函数后,要将无关紧要的套接字文件描述符关掉,如图所示:
10.5 分割 TCP 的 I/O 程序
我前面已经看完了 TCPIP网络编程这本书.

- muduo 学习笔记 834810071/muduo_study
- 基于 【高级】手写C++ Muduo网络库项目-掌握高性能网络库实现原理 代码
- GitHub - ShangyizhouA-Tiny-Network-Library 使用 C++ 11 重构陈硕的 muduo 库,去除 boost 依赖。内部实现有 HTTP 服务器,可支持 GET 请求。
- 可以参考这个项目,看它是如果实现用户的注册与登录的
- chenshuo/documents
- 有ppt https://github.com/iceCream1997/muduo_server_learn
- S1mpleBugmuduo_cpp11 C++11版本muduo网络库 除去了对boost库的依赖 还有视频教程
- 结合我自己写的一些c++的笔记 https://cpphub.roj.ac.cn
- 当然还有我们的cppreference 手册
- Hansimovlinux-server 《Linux 高性能服务器编程》和《Linux多线程服务端编程:使用muduo C++网络库》的笔记和源码,以及两个轻量级服务器的项目代码 也比较有用
- raichen/LinuxServerCodes: Linux高性能服务器编程源码 https://github.com/raichen/LinuxServerCodes
- chenshuo/muduo: Event-driven network library for multi-threaded Linux server in C++11 https://github.com/chenshuo/muduo
- qinguoyi/TinyWebServer: Linux下C++轻量级Web服务器 https://github.com/qinguoyi/TinyWebServer
- linyacool/WebServer: A C++ High Performance Web Server https://github.com/linyacool/WebServer
- 使用工具Webbench,进行测试