WebServer项目->linux网络编程基础知识
- 其中,遇到的错误总结
- 1). read:Connection reset by peer
- 2).什么叫连接被重置?
- 1. socket 介绍
- 2. 字节序
- 从主机字节序到网络字节序的转换函数:htons、htonl; 从网络字节序到主机字节序的转换函数:ntohs、ntohl。 其实,各个字符代表的含义: h - host 主机,主机字节序 to - 转换成什么 n - network 网络字节序 s - short unsigned short l - long unsigned int
- 3. socket 地址
- 通用 socket 地址
- 专用 socket 地址
- UNIX 本地域协议族使用如下专用的 socket 地址结构体:
- TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
- 1) struct sockaddr_in
- 2) struct in_addr
- 3) struct sockaddr_in6
- 4. IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)
- 5.TCP通信流程
- 6. 套接字函数
- 接下来写两个代码,一个是客户端,一个是服务端,使得两端进行通信
- 7. TCP 三次握手
- 8. TCP 滑动窗口
- 滑动窗口简介
- 滑动窗口的理解
- 客户端与服务器在通信时的滑动窗口一定是一样大的吗?
- 举个例子
- 9. TCP 四次挥手
- 10. TCP 通信并发
- 11. TCP 状态转换
- 12. 端口复用
- 13. 常看网络相关信息的命令
其中,遇到的错误总结
1). read:Connection reset by peer
原因:当你在使用网络连接时,如果出现 “Connection reset by peer” 的提示,通常表示在数据传输过程中,对方服务器已经关闭了连接,导致连接被重置。这可能是由于对方服务器上的一些问题,例如异常关闭、超时、负载过高等。
当这种情况发生时,你可以尝试重新连接或者稍后再次尝试连接。同时,你也可以通过检查服务器是否可用、网络连接是否稳定等手段来尽可能地避免这种情况的发生。
2).什么叫连接被重置?
连接被重置是指在进行网络通信时,一方(例如客户端)向另一方(例如服务器)发送了请求,但在(客户端)收到(服务端的)响应之前,(客户端)连接意外地中断或被迫关闭,导致(服务端的)数据无法传输。这种情况可能是由于多种原因引起的,例如网络故障、服务器过载、防火墙设置等等。
当连接被重置时,客户端和服务器都会收到一个错误消息,通常是“连接被重置”或“ERR_CONNECTION_RESET”。这种情况下,客户端需要重新发出请求以尝试重新建立连接,或者使用其他方法解决问题,例如检查网络连接是否正常、清除浏览器缓存等等。
由1)与2)可知,只要客户端发给服务端信息,且服务端也回了客户端信息,这时客户端再断开连接,服务端就不会出现read:Connection reset by peer这样的错误!
1. socket 介绍
socket(套接字)
socket 本身有“插座”的意思
- 套接字通信分两部分:
- 服务器端:被动接受连接,一般不会主动发起连接
- 客户端:主动向服务器发起连接
2. 字节序
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 <stdio.h>
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还没有学到)
网络字节序采用大端排序方式
从主机字节序到网络字节序的转换函数:htons、htonl;
从网络字节序到主机字节序的转换函数:ntohs、ntohl。
其实,各个字符代表的含义:
h - host 主机,主机字节序
to - 转换成什么
n - network 网络字节序
s - short unsigned short
l - long unsigned int
对应的函数名称及参数
#include <arpa/inet.h>
// 转换端口
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 <stdio.h>
#include <arpa/inet.h>
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;
}
3. socket 地址
socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个
socket地址。
客户端 -> 服务器(IP, Port)
通用 socket 地址
用的很少,了解即可
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;
其中,sa_family 成员的值如下:
- AF_UNIX 表示UNIX本地
- AF_INET 表示ipv4
- AF_INET6 表示ipv6
专用 socket 地址
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和
IPv6:
#include <netinet/in.h>
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))
1) struct sockaddr_in
struct sockaddr_in是一个用于表示Internet协议(IP)地址和端口号的结构体。它定义在头文件 #include <netinet/in.h> 中,并被广泛用于网络编程中。
sockaddr_in 结构体的定义如下:
#include <netinet/in.h>
struct sockaddr_in {
short sin_family; // 地址簇:AF_INET
unsigned short sin_port; // 端口号,使用网络字节序存储
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字段,通常为0
};
其中,成员变量的含义如下:
- sin_family:表示地址族,对于IPv4地址,该值应该使用常量 AF_INET。
- sin_port:表示端口号,使用网络字节序(big endian)存储,因此需要使用htons()函数进行转换。
- sin_addr:表示IPv4地址,类型为struct in_addr,可以使用inet_aton()或inet_addr()函数将字符串类型IP地址转化为二进制IP地址,也可以通过in_addr_t类型的变量直接赋值。
- sin_zero:填充字段,通常在调用bind()函数时将其赋值为0。
sockaddr_in 结构体可以用于创建套接字(socket)并绑定IP地址和端口号,也可以用于建立连接(connect),发送数据(sendto、send)和接收数据(recvfrom、recv)等操作。
2) struct in_addr
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_aton():将点分十进制的IPv4地址转换为网络字节序的二进制形式
- inet_addr():与inet_aton()类似,但返回值类型为in_addr_t
- inet_ntoa():将网络字节序的二进制形式的IPv4地址转换为点分十进制形式的字符串
需要注意的是,以上三个函数都已经被官方标记为不安全、过时,因此在编写网络程序时应该选择其他更加安全可靠的替代方案,如 inet_pton()
和 inet_ntop() 等函数。
3) struct sockaddr_in6
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; // 作用域标识符
};
其中,成员变量的含义如下:
- sin6_family:表示地址族,对于IPv6地址,该值应该使用常量 AF_INET6。
- sin6_port:表示端口号,使用网络字节序(big endian)存储,因此需要使用htons()函数进行转换。
- sin6_flowinfo:表示流信息,通常为0。
- sin6_addr:表示IPv6地址,类型为struct in6_addr,可以使用inet_pton()函数将字符串类型IP地址转化为二进制IP地址,也可以通过in6_addr类型的变量直接赋值。
- sin6_scope_id:表示作用域标识符,一般用于区分同一主机上不同的网络接口。
sockaddr_in6 结构体可以用于创建套接字(socket)并绑定IPv6地址和端口号,也可以用于建立连接(connect),发送数据(sendto、send)和接收数据(recvfrom、recv)等操作。
4. IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)
在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 <stdint.h>
//或者
#include <cstdint>
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 <arpa/inet.h>
// 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 是一样的
5.TCP通信流程
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
TCP是一种流式传输协议,UDP则是一种报式传输协议。
报式传输协议和流式传输协议是两种不同的数据传输方式:
- 报式传输协议(Packet-oriented Protocol):数据以固定大小的块(报文包)进行传输,每个报文包都包含了一个完整的数据包。在传输过程中会对每个报文包进行标记,这些标记可以用来检测和修复丢失的数据包。报式传输协议通常用于可靠性要求较高的应用程序,例如文件传输、电子邮件等。
- 流式传输协议(Stream-oriented Protocol):数据以连续的流的形式传输,没有固定的块大小,也没有明确的分割线。在传输过程中不会对数据进行分段或者分包,所有数据都被看作是一个连续的流。流式传输协议通常用于实时应用程序,例如音频和视频流、网络游戏等。
- 在TCP/IP协议栈中,TCP是一种流式传输协议,UDP则是一种报式传输协议。TCP通过发送和确认数据包来保证可靠性,并且提供了拥塞控制、流量控制等机制,因此适用于需要高度可靠性和有序性的应用程序。而UDP则没有这些机制,它快速传输数据包,适用于实时性要求比较高的应用程序,但是可能会出现数据包的丢失或者乱序。
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
// 客户端 (主动请求连接的角色)
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
6. 套接字函数
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
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 <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
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 <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
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;
}
7. TCP 三次握手
三次握手的目的是保证双方互相之间建立了连接。
第一次握手:
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):
- 一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从 B 到 A)的 TCP 报文段的序号值也具有相同的含义。
-
32 位确认号(acknowledgement number):
- 用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。
-
6 位标志位我们需要掌握的几位:
- ACK 标志: <用来做确认的>,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
- SYN 标志:<用来做连接的>表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
- FIN 标志:<用来做四次挥手的,即用来断开连接的>表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
-
16 位窗口大小(window size):<与滑动窗口有关>
- 是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
8. TCP 滑动窗口
滑动窗口简介
滑动窗口(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次,第四次回收,客户端同意了服务器端的断开请求
9. TCP 四次挥手
简单理解
四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。
进一步理解:
在TCP连接中,四次挥手是用于终止已经建立的连接的过程。与三次握手不同,四次挥手是需要客户端和服务器双方都发送 FIN 和 ACK 报文来完成的。
在程序中,任何一方可以主动调用 close() 函数来关闭连接。当其中一方调用了 close() 函数后,会向对方发送一个 FIN 报文,通知对方自己已经没有数据要发送了,并且请求对方关闭连接。对方接收到 FIN 报文后,会向发送方发送一个 ACK 报文,表示确认已经收到了 FIN 报文。这时,对方也会发送一个 FIN 报文给发送方,请求发送方也关闭连接。发送方收到对方的 FIN 报文后,同样会发送一个 ACK 报文表示确认收到了对方的 FIN 报文。最终,双方都成功关闭了连接。
需要注意的是,在四次挥手期间,如果某一方超时没有收到对方的响应报文,就会重试发送 FIN 报文。同时,为了避免出现网络拥塞和数据丢失等情况,还需要进行流量控制和拥塞控制等相关处理。因此,在实际编程中,需要考虑这些问题,以保证连接的可靠性和稳定性。
10. TCP 通信并发
要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。
思路一:
- 一个父进程,多个子进程
- 父进程负责:等待并接受客户端的连接
- 子进程负责:完成通信,接受一个客户端连接,就创建一个子进程用于通信。
这种模型被称为 Pre-forking 或 Pre-forked Worker,它是实现 TCP 通信并发的一种常见方式。
具体来说,这种模型包括一个父进程和多个子进程。父进程负责监听客户端的连接,并接受新的连接请求。一旦有新的连接请求到来,父进程就会 fork() 出一个新的子进程,该子进程负责与客户端进行通信,而父进程则继续等待下一个连接请求的到来。
其他思路:
-
多线程模型:使用多个线程来处理不同的连接或请求,每个线程负责单独的一部分工作。这种方法需要考虑线程安全和同步问题,同时需要合理控制线程的数量,避免过多的线程占用系统资源。
-
多进程模型:使用多个进程来处理不同的连接或请求,每个进程负责单独的一部分工作。这种方法可以有效利用多核 CPU 的优势,但也会带来进程间通信和资源管理等方面的问题。
-
异步非阻塞模型:使用异步编程技术来实现 TCP 通信,并辅以非阻塞 I/O 操作。这种方法可以极大地提高系统的并发能力,但需要对事件驱动模型、回调函数等方面有一定的掌握。
-
单线程模型:使用单个线程来处理所有的连接或请求,通过轮询方式进行处理。这种方式适用于连接数较少但请求响应时间较长的场景,例如 Web 应用中的长轮询。
-
基于 Reactor 模式的模型:将 TCP 通信拆分为多个事件和状态,通过 Reactor 模式来管理、调度和处理这些事件和状态。这种方式可以保证系统的高性能和可伸缩性,但需要对网络编程和底层技术有一定的了解。
11. TCP 状态转换
- 2MSL(Maximum Segment Lifetime)
主动断开连接的一方, 最后进入一个 TIME_WAIT状态, 这个状态会持续: 2msl
msl: 官方建议: 2分钟, 实际是30s
这样就能够让 TCP 连接的主动关闭方(在上图中,指的是是左边的那一方)在它发送的 ACK 丢失的情况下重新发送最终的 ACK。
主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,
被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是
重传 FIN 直到它收到一个最终的 ACK。
- 半关闭
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发
送的数据,但是 A 已经不能再向 B 发送数据。
从程序的角度,可以使用 API 来控制实现半连接状态:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
//shutdown函数的参数:
- sockfd: 需要关闭的socket的描述符
- how: 允许为shutdown操作选择以下几种方式:
– SHUT_RD(0):不让读 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
– SHUT_WR(1): 不让写 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
–SHUT_RDWR(2):不让读,也不让写关闭sockfd的读写功能。相当于调用shutdown两次:首先调用SHUT_RD,然后再调用SHUT_WR。
注:
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
- 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
进程都调用了 close,套接字将被释放。- 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法通过此sfd进行通信。
但如果一个进程 调用了close(sfd) 将不会影响到其它进程。
12. 端口复用
端口复用最常用的用途是:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
- sockfd : 要操作的文件描述符
- level : 级别 - SOL_SOCKET (端口复用的级别)
- optname : 选项的名称(下面两个都可以用)
- SO_REUSEADDR:作用于单个套接字,可以让处于 TIME_WAIT 状态下的套接字绑定到同一端口上。
SO_REUSEADDR 选项表示允许一个套接字(socket)或者服务端口(server port)绑定到一个已经使用的地址上,只要该地址处于 TIME_WAIT 状态且套接字没有任何连接依赖关系。这种情况通常发生在服务器关闭连接后,套接字可能需要在一段时间内等待操作系统释放资源,才能被另一个进程重用。使用 SO_REUSEADDR 可以让新套接字和旧套接字共用同一个地址,从而避免 “Address already in use” 错误。 - SO_REUSEPORT:作用于多个套接字,使得多个进程可以同时绑定到同一端口上,从而实现负载均衡或故障转移等。
SO_REUSEPORT 选项则可以让多个进程同时绑定到一个端口上,从而实现负载均衡或故障转移等功能。在使用 SO_REUSEPORT 选项时,多个套接字必须绑定到同一个 IP 地址和端口号上,并且每个套接字都可以独立处理传入的连接请求。
- SO_REUSEADDR:作用于单个套接字,可以让处于 TIME_WAIT 状态下的套接字绑定到同一端口上。
- optval : 端口复用的值(整形)
- 1 : 可以复用
- 0 : 不可以复用
- optlen : optval参数的大小
注意一点:
端口复用,设置的时机是在服务器绑定端口之前。
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));
13. 常看网络相关信息的命令
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器
查看 客户端/服务器 的状态:netstat -anp | grep 9999