原因:当你在使用网络连接时,如果出现 “Connection reset by peer” 的提示,通常表示在数据传输过程中,对方服务器已经关闭了连接,导致连接被重置。这可能是由于对方服务器上的一些问题,例如异常关闭、超时、负载过高等。
当这种情况发生时,你可以尝试重新连接或者稍后再次尝试连接。同时,你也可以通过检查服务器是否可用、网络连接是否稳定等手段来尽可能地避免这种情况的发生。
连接被重置是指在进行网络通信时,一方(例如客户端)向另一方(例如服务器)发送了请求,但在(客户端)收到(服务端的)响应之前,(客户端)连接意外地中断或被迫关闭,导致(服务端的)数据无法传输。这种情况可能是由于多种原因引起的,例如网络故障、服务器过载、防火墙设置等等。
当连接被重置时,客户端和服务器都会收到一个错误消息,通常是“连接被重置”或“ERR_CONNECTION_RESET”。这种情况下,客户端需要重新发出请求以尝试重新建立连接,或者使用其他方法解决问题,例如检查网络连接是否正常、清除浏览器缓存等等。
由1)与2)可知,只要客户端发给服务端信息,且服务端也回了客户端信息,这时客户端再断开连接,服务端就不会出现read:Connection reset by peer这样的错误!
1)简介
字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。
字节序举例
1)小端字节序
eg: 0x 01 02 03 04
内存的方向: ----->
内存的低位 -----> 内存的高位
04 03 02 01(低位内存存低位数据,高位内存存高位数据)
2)大端字节序
eg: 0x 01 02 03 04
内存的方向 ----->
内存的低位 -----> 内存的高位
01 02 03 04(低位内存存高位数据,高位内存存低位数据)
2)用代码查看自己电脑是大端还是小端字节序
/*
字节序:字节在内存中存储的顺序。
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/
// 通过代码检测当前主机的字节序
#include
int main() {
union {
short value; // 2字节
char bytes[sizeof(short)]; // char[2]
} test;
test.value = 0x0102;
if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
printf("大端字节序\n");
} else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
printf("小端字节序\n");
} else {
printf("未知\n");
}
return 0;
}
3)字节序转换函数(用于后面点分十进制的ipv4地址字节序的转换,ipv6还没有学到)
网络字节序采用大端排序方式
对应的函数名称及参数
#include
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序
注:转端口的字节序在tcp连接中经常用到;但是,转ip的字节序还没用到,ip的操作在后边是一句话搞定的,如下:
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr.s_addr);//"127.0.0.1"=>指定服务端的ip地址
eg:
/*
网络通信时,需要将主机字节序转换成网络字节序(大端),
另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序
*/
#include
#include
int main() {
// hton s 转换端口
unsigned short a = 0x0102;
printf("a : %04x\n", a);
unsigned short b = htons(a);
printf("b : %04x\n", b);
printf("=======================\n");
// hton l 转换IP
char buf[4] = {192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
printf("=======================\n");
// ntoh l 转换IP
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
printf("=======================\n");
// ntoh s 转换端口
unsigned short a1 = 0x0201;
printf("a1 : %04x\n", a1);
unsigned short b1 = htons(a1);
printf("b1 : %04x\n", b1);
return 0;
}
socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个
socket地址。
客户端 -> 服务器(IP, Port)
用的很少,了解即可
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;
其中,sa_family 成员的值如下:
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include
struct sockaddr_un
{
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和
IPv6:
#include
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
struct sockaddr_in是一个用于表示Internet协议(IP)地址和端口号的结构体。它定义在头文件 #include
sockaddr_in 结构体的定义如下:
#include
struct sockaddr_in {
short sin_family; // 地址簇:AF_INET
unsigned short sin_port; // 端口号,使用网络字节序存储
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字段,通常为0
};
其中,成员变量的含义如下:
sockaddr_in 结构体可以用于创建套接字(socket)并绑定IP地址和端口号,也可以用于建立连接(connect),发送数据(sendto、send)和接收数据(recvfrom、recv)等操作。
struct in_addr是一个用于表示IPv4地址的结构体,包含一个成员变量s_addr,类型为in_addr_t,用于存储32位的IPv4地址。
struct in_addr被struct sockaddr_in 包含
其定义如下:
struct in_addr {
in_addr_t s_addr; // IPv4地址,使用网络字节序存储
};
在使用 struct sockaddr_in 结构体时,常常需要将IPv4地址转化为二进制形式(即网络字节序),或者将二进制形式的IPv4地址转化为字符串形式。这可以通过以下函数完成:
需要注意的是,以上三个函数都已经被官方标记为不安全、过时,因此在编写网络程序时应该选择其他更加安全可靠的替代方案,如 inet_pton()
和 inet_ntop() 等函数。
struct sockaddr_in6是一个用于表示IPv6地址和端口号的结构体,类似于 sockaddr_in。它定义在头文件 netinet/in.h 中,并被广泛用于IPv6网络编程中。
sockaddr_in6 结构体的定义如下:
struct sockaddr_in6 {
uint16_t sin6_family; // 地址簇:AF_INET6
uint16_t sin6_port; // 端口号,使用网络字节序存储
uint32_t sin6_flowinfo; // 流信息,通常为0
struct in6_addr sin6_addr; // IPv6地址
uint32_t sin6_scope_id; // 作用域标识符
};
其中,成员变量的含义如下:
sockaddr_in6 结构体可以用于创建套接字(socket)并绑定IPv6地址和端口号,也可以用于建立连接(connect),发送数据(sendto、send)和接收数据(recvfrom、recv)等操作。
在2.字节序的 3)我们学了ip地址字节序的转换,
这节我们学习,将点分十进制的ipv4 转成 struct sockaddr_in 里面的 sin_port —>这是一个uint32_t类型,下面是一个关于uint32_t类型的代码:
uint32_t是C/C++中定义的一种无符号整数类型(unsigned integer),
它可以表示0到4294967295之间的整数。在32位编译环境下,它的长度为32位,因此称为"uint32_t"。
#include
//或者
#include
uint32_t address = 192.168.1.1; //定义一个IPv4地址变量
接下来介绍一下,inet_pton函数:
先举一个例子:
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;//指的是ipv4
//将ipv4类型的地址"127.0.0.1",转换成uint32_t并存放在serveraddr.sin_addr.s_addr里边
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr.s_addr);//"127.0.0.1"=>指定服务端的ip地址
//设置端口,htons()=>将端口的主机字节序 转换成 网络字节序
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
下面来详细写一下:inet_pton函数与inet_ntop函数
#include
// p:主机字节序的ip, 转换成 n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst); //后边常用!!!///
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// n:表示network,网络字节序的整数, 转换成 p:主机字节序的ip
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
TCP是一种流式传输协议,UDP则是一种报式传输协议。
报式传输协议和流式传输协议是两种不同的数据传输方式:
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
// 客户端 (主动请求连接的角色)
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
#include
#include
#include // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
int listen(int sockfd, int backlog);
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 如:5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
// server.c
#include
#include
#include
#include
#include
int main() {
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//ipv4
saddr.sin_port = htons(9999);//指定端口9999,且将主机字节序转换成网络字节序(大端字节序)
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);//将"192.168.193.128"转化成ipv4格式的int_addr_t格式
saddr.sin_addr.s_addr = INADDR_ANY; //指定可以连接进来的客户端ip 0.0.0.0 =>表示所有客户端都可以连接进来
//struct sockaddr_in类型的诞生,方便了我们开发,但是bind的第二个参数要求的还是(struct sockaddr *)类型,所以还是要强制类型转换一下
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 8);//第二个参数是未连接的和已经连接的和的最大值,我们设置8已经足够了
if(ret == -1) {
perror("listen");
exit(-1);
}
printf("begin to listen...\n");
// 4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];//3+1+3+1+3+1+3+'\0'=>点分十进制ipv4的大小
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);//clientaddr.sin_port端口
printf("client ip is %s, port is %d\n", clientIP, clientPort);
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data : %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("clinet closed...\n");
break;
}
char * data = "hello,i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
// client.c
#include
#include
#include
#include
#include
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
//inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr.s_addr);//"127.0.0.1"=>指定服务端的ip地址
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024] = {0};
while(1) {
char * data = "hello,i am client";
// 给客户端发送数据
write(fd, data , strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...\n");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
第一次握手:
1.客户端将SYN标志位置为1
2.生成一个随机的32位的序号seq=J , 这个序号后边是可以携带数据(数据的大小)
第二次握手:
1.服务器端接收客户端的连接: ACK=1
2.服务器会回发一个确认序号: ack=客户端的序号 + 数据长度 + SYN/FIN(按一个字节算)<只有上一次收到SYN或FIN标志时,ack确认序号才额外会加1;其余的只会加上一个具体的数据( 客户端的序号 + 数据长度 )>
3.服务器端会向客户端发起连接请求: SYN=1
4.服务器会生成一个随机序号:seq = K
第三次握手:
1.客户单应答服务器的连接请求: ACK=1
2.客户端回复收到了服务器端的数据:ack=服务端的序号 + 数据长度 + SYN/FIN(按一个字节算)
32 位序号(sequence number):
32 位确认号(acknowledgement number):
6 位标志位我们需要掌握的几位:
16 位窗口大小(window size):<与滑动窗口有关>
滑动窗口(Sliding window)是一种用于流量控制和拥塞控制的技术,常用于数据传输的可靠性协议中,如TCP。
滑动窗口可以让发送方和接收方之间的数据传输更加高效,并能够自适应网络拥塞的情况。
在滑动窗口中,发送方和接收方之间会定义一个固定大小的窗口,用来管理待传输的数据包。每当发送方成功发送一个数据包时,它就会将窗口向前滑动一段距离(通常为已确认的数据字节数),以便发送更多的数据。 而接收方则会向发送方发送一个确认消息,告知其已经成功接收了某个数据包,从而使得发送方可以继续向前滑动窗口,发送新的数据包。
通过使用滑动窗口,发送方和接收方之间可以实现流量控制和拥塞控制,从而避免因网络拥塞而导致的数据包丢失和重传,提高数据传输的效率和可靠性。同时,滑动窗口还可以根据网络状况自适应地调整窗口大小,以适应不同的带宽和延迟环境。
需要注意的是,滑动窗口并不是一种具体的实现方式,而是一种通用的概念和技术。实际上,在不同的协议和系统中,滑动窗口的具体实现方式可能有所不同。
窗口理解为缓冲区的大小
滑动窗口的大小会随着发送数据和接收数据而变化。
通信的双方都有发送缓冲区和接收数据的缓冲区
服务器:
发送缓冲区(发送缓冲区的窗口)
接收缓冲区(接收缓冲区的窗口)
客户端:
发送缓冲区(发送缓冲区的窗口)
接收缓冲区(接收缓冲区的窗口)
不一定,客户端与服务器在通信时的滑动窗口大小可以根据具体情况设置不同的值。
通常情况下,服务器端的滑动窗口大小会比客户端的要大。这是因为在网络应用中,服务器经常需要处理来自多个客户端的请求,并向它们发送响应数据。如果服务器的滑动窗口过小,则可能无法及时处理来自客户端的请求,影响系统的性能和响应时间。因此,在服务器端,通常会将滑动窗口大小设置得比较大,以便同时处理多个客户端的请求。
另一方面,客户端的滑动窗口大小则取决于网络环境和应用需求。例如,在一个高延迟的网络中,如果客户端的滑动窗口过小,则可能会导致传输速度变慢;而如果滑动窗口过大,则可能会导致拥塞和数据丢失。因此,需要根据具体情况调整滑动窗口大小,以达到最优的传输效率和可靠性。
解释
# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 发送端目前可用的滑动窗口大小 (注意:客户端和服务器都可以作为发送端)
- 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
- 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
- 第三次握手
- 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
- 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口内可用大小是2k
- 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口内可用大小是4k
- 第12次,客户端给服务器发送了1k的数据
- 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
- 第14次,服务器回复ACK 8194, 还传递了以下消息:
①a:同意断开连接的请求
②b:告诉客户端已经接受到方才发的2k的数据
③c:滑动窗口内可用大小是2k- 第15、16次,通知客户端滑动窗口内可用大小分别是4k、6k
- 第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
- 第18次,第四次回收,客户端同意了服务器端的断开请求
简单理解
四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。
进一步理解:
在TCP连接中,四次挥手是用于终止已经建立的连接的过程。与三次握手不同,四次挥手是需要客户端和服务器双方都发送 FIN 和 ACK 报文来完成的。
在程序中,任何一方可以主动调用 close() 函数来关闭连接。当其中一方调用了 close() 函数后,会向对方发送一个 FIN 报文,通知对方自己已经没有数据要发送了,并且请求对方关闭连接。对方接收到 FIN 报文后,会向发送方发送一个 ACK 报文,表示确认已经收到了 FIN 报文。这时,对方也会发送一个 FIN 报文给发送方,请求发送方也关闭连接。发送方收到对方的 FIN 报文后,同样会发送一个 ACK 报文表示确认收到了对方的 FIN 报文。最终,双方都成功关闭了连接。
需要注意的是,在四次挥手期间,如果某一方超时没有收到对方的响应报文,就会重试发送 FIN 报文。同时,为了避免出现网络拥塞和数据丢失等情况,还需要进行流量控制和拥塞控制等相关处理。因此,在实际编程中,需要考虑这些问题,以保证连接的可靠性和稳定性。
要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。
思路一:
这种模型被称为 Pre-forking 或 Pre-forked Worker,它是实现 TCP 通信并发的一种常见方式。
具体来说,这种模型包括一个父进程和多个子进程。父进程负责监听客户端的连接,并接受新的连接请求。一旦有新的连接请求到来,父进程就会 fork() 出一个新的子进程,该子进程负责与客户端进行通信,而父进程则继续等待下一个连接请求的到来。
其他思路:
多线程模型:使用多个线程来处理不同的连接或请求,每个线程负责单独的一部分工作。这种方法需要考虑线程安全和同步问题,同时需要合理控制线程的数量,避免过多的线程占用系统资源。
多进程模型:使用多个进程来处理不同的连接或请求,每个进程负责单独的一部分工作。这种方法可以有效利用多核 CPU 的优势,但也会带来进程间通信和资源管理等方面的问题。
异步非阻塞模型:使用异步编程技术来实现 TCP 通信,并辅以非阻塞 I/O 操作。这种方法可以极大地提高系统的并发能力,但需要对事件驱动模型、回调函数等方面有一定的掌握。
单线程模型:使用单个线程来处理所有的连接或请求,通过轮询方式进行处理。这种方式适用于连接数较少但请求响应时间较长的场景,例如 Web 应用中的长轮询。
基于 Reactor 模式的模型:将 TCP 通信拆分为多个事件和状态,通过 Reactor 模式来管理、调度和处理这些事件和状态。这种方式可以保证系统的高性能和可伸缩性,但需要对网络编程和底层技术有一定的了解。
#include
int shutdown(int sockfd, int how);
//shutdown函数的参数:
注:
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
- 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
进程都调用了 close,套接字将被释放。- 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法通过此sfd进行通信。
但如果一个进程 调用了close(sfd) 将不会影响到其它进程。
端口复用最常用的用途是:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
#include
#include
// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
注意一点:
端口复用,设置的时机是在服务器绑定端口之前。
setsockopt();
bind();
eg:
//端口复用
//int optval = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
//端口复用
int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器
查看 客户端/服务器 的状态:netstat -anp | grep 9999