目录
代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. TCP三次握手
TCP: 面向连接的, 安全的, 流式传输协议
- 面向连接:三次握手、四次挥手
- 安全的:通信过程中会进行数据校验 -> 丢失之后会重传
- 流式:双方每次操作的数据量可以不同
SYN: 发起一个--建立连接的请求
ACK: 确认、同意
FIN: 发起一个--断开连接的请求
如果这些标志位在协议中 == 1 => 请求被发起,
== 0 => 没有发起这个请求
seq: 序号, 随机生成:随机数
ack: 确认序号, 确认tcp通信过程中接收的数据的量
/*
第一次握手: 客户端发起的
- 将SYN设置为1, 向服务器发起连接请求
- 生成了随机序号: seq=J
第二次握手:
- 回复ACK=1, 接受了客户端的连接请求
- 发送SYN=1, 服务器向客户端发起一个连接请求
- ack=J+1, J(第一次握手生成的随机序号),
+1代表接收了一个字节(连接请求SYN)
- seq=K, 服务器端生成一个随机序号K
第三次握手:
- ACK=1, 客户端接受了服务器的连接请求
- ack=K+1, K(第2次握手生成的随机序号),
+1代表接收了一个字节(连接请求SYN)
*/
2. TCP四次挥手
// 四次挥手的时候, 首先断开连接的一方: 可以是客户端也可以是服务器
// 在程序中如何挥手: close();
// 过程: 假设客户端先断开连接
1. 客户端调用close函数, 在协议中 FIN 被设置为 1, 发送给服务器
2. 服务器接收到客户端断开连接的请求, 同意断开连接, 在tcp协议中 标志位 ACK = 1
3. 服务器端调用close函数, 在协议中 FIN 被设置为 1, 发送给客户端
4. 客户端接收到服务器断开连接的请求, 同意断开连接, 在tcp协议中 标志位 ACK = 1
3. TCP滑动窗口
1.滑动窗口是 TCP 中用于实现诸如:ACK确认、流量控制、拥塞控制的承载结构;
2.窗口理解为一块缓存就可以了;
3.滑动窗口的内存是在变化的 -> 存储的数据量在变化;
4.通信的双方都有滑动窗口;
服务器
读缓冲区 -> 内核中的内存
被填满之后, 阻塞客户端的发送(写缓冲区)
写缓冲区 -> 内核中的内存
客户端
读缓冲区 -> 内核中的内存
被填满之后, 阻塞服务器的发送(写缓冲区)
写缓冲区 -> 内核中的内存
这个图是一个单向的数据发送:
# 发送端 -> 图中对应的是写缓冲区
- 白色的格子: 空闲的没有数据的空间
- 灰色的格子: 代表已经发送出去的数据
- 粉色的格子: 还没有发送出去的数据
- 全部为粉色: 写阻塞
- 对方的读缓冲区满了, 写被阻塞
# 接收端 -> 图中对应的是读缓冲区
- 白色的格子: 空闲的没有数据的空间
- 粉色格子: 接收到的数据, 但是这个数据还没有被处理
- 写满之后, 会阻塞发送端的发送
fast sender -> 客户端
slow recv -> 服务器
# mss: 最大的数据段大小 Maximum Segment Size -> 一条数据的最大长度
# win: 滑动窗口
1. 发送SYN请求和服务器建立连接, 客户端的自己的滑动窗口大小4K(缓存4k数据),
客户端发送最大字节数1460,0是客户端生成的随机序号
2. 接受客户端连接请求, 发送SYN请求和客户端建立连接, 服务器的自己的滑动窗口
大小6K(缓存6k数据), 服务器发送最大字节数1K, 8000是服务器生成的随机序号
3. 接受服务器连接请求, 客户端的自己的滑动窗口大小4K
4. 4-9步客户端不停的给服务器发送数据, 每次发送1k
服务器的滑动窗口为6k, 接收了6k数据, 满了, 因此将发送端阻塞
5. 第10步: 客户端发送给服务器的6145个字节全部被接收, 服务器滑动窗口可用缓存为2k
6. 第11步: 客户端发送给服务器的6145个字节全部被接收, 服务器滑动窗口可用缓存为4k
7. 第12步: 客户端由给服务器发送了1k数据
8. 第13步: 客户端由给服务器发送了1k数据, 并且主动和服务器断开连接, 发送FIN
9. 14-16: 服务器接受了客户端断开连接的请求, 一直在处理滑动窗口缓存中的数据
10. 17步: 第三次挥手
11. 18步: 第四次挥手
4. TCP通信并发
1.1多进程 -> client/server 实现
// 多进程思路:父进程、子进程
/*
父进程: 一直不停的等待并接受客户端的连接
- accept
- 成功建立连接之后, fork()
子进程: 通信
- read , write
*/
/*
多进程程序(有血缘关系)特点:
-地址空间被拷贝
- 所有的数据被复制, 父子进程互不影响
- 文件描述符表是共享的(父进程拷贝给子进程的那部分)
*/
// 服务器端的程序
int main()
{
// 1. 创建监听的套接字
int lfd = socket(af_int, sock_stream, 0);
// 2. 绑定IP和端口
bind(lfd, localaddr, sizeof(struct sockaddr));
// 3. 设置监听
listen(lfd, 128);
while(1)
{
// 一直检测有没有客户端连接, 如果有建立连接
int cfd = accept(lfd, cliaddr, &len);
// 成功建立了连接, 创建子进程 -> 通信
pid_t pid = fork();
if(pid > 0)
{
// 父进程利用信号捕捉的回调函数, 回收资源pcb
}
else if(pid == 0)
{
// 子进程, 通信
while(1)
{
read(cfd, buf, sizeof(buf));
write(cfd, data, strlen(data));
//跳出循环的条件
...
}
}
}
}
02client.c
// 02client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
//1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
exit(0);
}
//2.连接服务器
struct sockaddr_in serverAddr; //服务器的相关数据
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.184.132", &serverAddr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (ret == -1)
{
perror("connect");
exit(0);
}
//3.通信
while (1)
{
//3.1.发送数据
char *p = "你好服务器...";
write(fd, p, strlen(p) + 1);
//3.2.接受数据
char buf[1024];
int len = read(fd, buf, sizeof(buf));
if (len > 0)
{
printf("recv data:%s\n", buf);
}
else if (len == 0)
{
printf("server disconnect...\n");
break;
}
else
{
perror("read");
exit(0);
}
sleep(1);
}
close(fd);
return 0;
}
02multi_process_server.c
// 02multi_process_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
//7.2.回调函数--回收子进程资源
void callback(int num)
{
while (1)
{
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid == 0 || pid == -1)
{
//还有子进程 或者 回收完毕
break;
}
printf("回收的子进程PID:%d\n", num);
}
}
int main()
{
//1.创建监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
exit(0);
}
//2.绑定本地IP和端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; //地址族协议
server_addr.sin_port = htons(9999); //端口
inet_pton(fd, "192.168.184.132", &server_addr.sin_addr.s_addr);
int ret = bind(fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (ret == -1)
{
perror("bind");
exit(0);
}
//3.设置监听
ret = listen(fd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
//7.1.注册信号捕捉
//当子进程退出时,会给父进程发送SIGCHLD信号
//我们捕捉,然后回收子进程资源
struct sigaction act;
act.sa_flags = 0; // 使用自定义回调函数
sigemptyset(&act.sa_mask);
act.sa_handler = callback; //自定义的回调函数
sigaction(SIGCHLD, &act, NULL);
while (1)
{
//主进程:等待客户端连接,并进行处理
//4.阻塞等待客户端连接,并且接受建立连接
struct sockaddr_in client_addr;
int len1 = sizeof(client_addr);
printf("等待客户端连接....\n");
int cfd = accept(fd, (struct sockaddr *)&client_addr, &len1);
if (cfd == -1)
{
if (errno == EINTR)
{ //如果是信号导致的函数调用失败,就continue
// 即:重新调用accept函数
continue;
}
perror("accept");
exit(0);
}
//5.如果连接成功,创建子进程
pid_t pid = fork();
if (pid == 0)
{
//子进程部分:通信
//6.通信
while (1)
{ //6.1从父进程复制过来的父进程用来监听连接请求的文件描述符,没用处
close(fd);
//6.2.接收数据部分
char buf[24];
int len = read(cfd, buf, sizeof(buf));
if (len > 0)
{ //客户端IP
char clientIP[17];
inet_ntop(AF_INET,
&client_addr.sin_addr.s_addr,
clientIP,
sizeof(clientIP));
//客户端端口
unsigned short cliPORT = ntohs(client_addr.sin_port);
printf("recv data:%s,client IP:%s,Port:%d\n", buf, clientIP, cliPORT);
//6.3.发送数据
char msg[1024];
sprintf(msg, "你好客户端--%s:%d\n", clientIP, cliPORT);
write(cfd, msg, strlen(msg) + 1);
}
else if (len == 0)
{
printf("client disconnect...\n");
break;
}
else
{
perror("read");
break;
}
}
close(cfd); //关闭子进程文件描述符
exit(0); //子进程退出
}
else if (pid > 0)
{
//父进程代码
//父进程只负责监听连接请求,不负责通信
//父进程中用来通信的文件描述符--没用处
//所以:关闭父进程里的cfd
close(cfd);
}
}
//当有的子进程都关闭后,就关闭父进程监听的文件描述符
close(fd);
return 0;
}
1.2多线程 -> client/server 实现
// 多线程思路
/*
多线程: 主线程, 子线程
-主线程: 1个, 不停的接受客户端请求, 建立新连接 -> 创建一个子线程
-子线程: 接收数据、 发送数据
*/
/*
多线程程序线程之间:
- 共享用户区, 除了栈
- 全局数据区 -> 全局, 静态变量
- 堆区 -> malloc/new
- 每个线程都有自己的栈内存
*/
// 定义结构体, 存储要传送的数据
struct Info
{
int cfd; // 通信的文件描述符
struct sockaddr_in addr; // 客户端的IP和端口信息
};
// 定义全局变量
struct Info info[1024];
// 子线程对应的处理函数
void* callback(void* arg)
{
//接受传进来的参数
...
//传进来的数组元素,怎么置空
...
// 通信 -> 客户端
while(1)
{
// 接收数据
read(cfd, buf, sizeof(buf));
// 打印客户端的IP和端口
// 发送数据
}
}
// 服务器端的程序
int main()
{
// 1. 创建监听的套接字
int lfd = socket(af_int, sock_stream, 0);
// 2. 绑定IP和端口
bind(lfd, localaddr, sizeof(struct sockaddr));
// 3. 设置监听
listen(lfd, 128);
while(1)
{
// 一直检测有没有客户端连接, 如果有建立连接
int cfd = accept(lfd, cliaddr, &len);
// 成功建立了连接, 创建子线程 -> 通信
// 从数组中找一个没有被使用的元素
struct Info *pinfo = info[x];
pinfo->cfd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);
pthread_create(&tid, NULL, callback, pinfo);
}
03client.c
// 01server.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serverStruct;
serverStruct.sin_family = AF_INET;
serverStruct.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverStruct.sin_addr.s_addr);
int ret = connect(lfd, (struct sockaddr *)&serverStruct, sizeof(serverStruct));
if (-1 == ret)
{
perror("accept");
exit(-1);
}
char buf[1024];
char buf2[] = "你好,服务端...";
while (1)
{
write(lfd, buf2, strlen(buf2) + 1);
int len = read(lfd, buf, sizeof(buf));
if (len > 0)
{
printf("redc buf: %s\n", buf);
sleep(1);
}
else if (len == 0)
{
printf("server disconnect...\n");
break;
}
else
{
perror("read");
break;
}
}
close(lfd);
return 0;
}
03multi_pthread_server.c
// 01server.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <pthread.h>
typedef struct Info
{
int cfd;
struct sockaddr_in clientStruct;
} Info;
Info info[128];
void *callback(void *arg)
{
Info *pInfo = (Info *)arg;
while (1)
{
char buf[1024];
int len = read(pInfo->cfd, buf, sizeof(buf));
if (len > 0)
{
char clientIP[17];
inet_ntop(AF_INET,
&pInfo->clientStruct.sin_addr.s_addr,
clientIP,
sizeof(clientIP));
//客户端端口
unsigned short cliPORT = ntohs(pInfo->clientStruct.sin_port);
printf("recv data:%s,client IP:%s,Port:%d\n", buf, clientIP, cliPORT);
char buf2[] = "你好,客户端...";
write(pInfo->cfd, buf2, strlen(buf2) + 1);
}
else if (len == 0)
{
printf("client disconnect...\n");
break;
}
else
{
perror("read");
break;
}
}
close(pInfo->cfd);
pInfo->cfd = -1;
return NULL;
}
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(-1);
}
struct sockaddr_in serverStruct;
serverStruct.sin_family = AF_INET;
serverStruct.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverStruct.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr *)&serverStruct, sizeof(struct sockaddr_in));
if (-1 == ret)
{
perror("bind");
exit(-1);
}
ret = listen(lfd, 5);
if (-1 == ret)
{
perror("listen");
exit(-1);
}
int lenInfo = sizeof(info) / sizeof(info[0]);
for (int i = 0; i < lenInfo; ++i)
{
memset(&info[i].clientStruct, 0, sizeof(struct sockaddr_in));
info[i].cfd = -1;
}
while (1)
{
struct sockaddr_in clientStruct;
unsigned int len = sizeof(clientStruct);
printf("等待客户端连接....\n");
int cfd = accept(lfd, (struct sockaddr *)&clientStruct, &len);
if (-1 == cfd)
{
if (errno == EINTR)
{
continue;
}
perror("accept");
exit(-1);
}
struct Info *pInfo;
for (int i = 0; i < lenInfo; ++i)
{
if (info[i].cfd == -1)
{
pInfo = &info[i];
break;
}
}
pInfo->cfd = cfd;
memcpy(&pInfo->clientStruct, &clientStruct, sizeof(clientStruct));
pthread_t tid;
pthread_create(&tid, NULL, callback, (void *)pInfo);
pthread_detach(tid);
}
close(lfd);
return 0;
}
2.通信细节(伪代码)
// 服务器端
// 出现阻塞的情况
- accept
有客户端连接解除阻塞
- read
1.双方保持连接时:对应的读缓冲区(内核)有数据, 解除阻塞, 否则一直阻塞
2.通信的另一方断开了连接时:read返回0,接收数据的一方不阻塞
- write
写缓冲区满了(内核), 阻塞, 否则不阻塞
while(1)
{
// 接收数据
char buf[24];
int len = read(cfd, buf, sizeof(buf));
if(len > 0)
{
// 接收到了对方的数据
printf("recv buf: %s\n", buf);
}
else if(len == 0)
{
printf("client disconnect ....\n");
break;
}
else
{
perror("read");
break;
}
// 发送数据
char *p = "你好, 客户端...\n";
write(cfd, p, strlen(p)+1);
}
// 阻塞函数在阻塞过程中突然被信号中断, 改变了原来的行为->去处理信号(信号优先级高)
// 当信号处理完毕之后, 再回到中断的位置之后,accept就不能再阻塞了, 因此返回-1
// 解决方案:
// 当函数由于信号被中断, 只需要知道errno被设置为多少, 我们就可以做对应的处理
// :errno == EINTR
while(1)
{
...
...
int cfd = accept(fd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1)
{
if(errno == EINTR)
{
// 重新调用accept
continue;//!!!!!
}
perror("accept");
exit(0);
}
...
...
}
5.TCP状态转换总结
/*
三次握手: 连接过程
还没有握手之前, 服务器端调用listen()函数, 状态: LISTEN
第一次:
客户端: 调用connect()函数, 发送了连接请求, 状态变成了: SYN_SNET
服务器收到客户端的连接请求: 服务器状态 LISTEN -> SYN_RCVD
第二次:
服务器同意客户端连接,回复ack, 并且向客户发起连接请求
客户端: 收到服务器的ack, 单向连接建立, 客户端状态: SYN_SNET -> ESTABLISHED
第三次握手:
服务器收到客户端的ack, 单向连接建立, 服务器状态: SYN_RCVD -> ESTABLISHED
四次挥手: 断开连接的过程
第一次挥手:
主动断开连接的一方: 调用close()函数, 状态变化: ESTABLISHED -> FIN_WAIT_1
被动断开连接的一方: 没有调用close()函数, 只是收到了FIN,
状态变化: ESTABLISHED -> CLOSE_WAIT
第二次挥手:
被动断开连接的一方: 回复一个ack, 状态仍为: CLOSE_WAIT
主动断开连接的一方: 收到ack, 状态变化: FIN_WAIT_1 -> FIN_WAIT_2
第三次挥手:
被动断开连接的一方: 调用close()函数, 发送FIN给对方, 状态: CLOSE_WAIT -> LAST_ACK
主动断开连接的一方: 状态变化: FIN_WAIT_2 -> TIME_WAIT
第四次挥手:
主动断开连接的一方: 回复ack, 状态没变: TIME_WAIT
被动断开连接的一方: 收到ACK,进程退出...
通信过程中: 状态是不变的, 状态: ESTABLISHED, 通信过程中, 必须是这种状态
*/
- 2MSL(Maximum Segment Lifetime)
- 主动断开连接的一方最后的状态 -> TIME_WAIT
- 报文时间长度: 默认是2分钟, 实际是30秒
- 因此主动断开连接的一方, 最后会等待1分钟(2msl), 之后才退出
当TCP连接主动关闭方接收到被动关闭方发送的FIN和最终的ACK后,连接的主动关闭方必须处于TIME_WAIT状态并持续2MSL时间。
这样就能够让TCP连接的主动关闭方在它发送的ACK丢失的情况下重新发送最终的ACK。
主动关闭方重新发送的最终ACK并不是因为被动关闭方重传了ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的FIN。事实上,被动关闭方总是重传FIN直到它收到一个最终的ACK。
半关闭
当TCP链接中A向B发送 FIN 请求关闭,另一端B回应ACK之后,并没有立即发送 FIN 给A, A方处于半连接状态(半开关),此时A可以接收B发送的数据,但是A已经不能再向B发送数据。
// tcp通信过程中, 服务器和客户端建立的是双向连接
// 如果一方的连接断开(调用了close()函数), 这种状态就叫半关闭
// 特点: 调用close()函数的一方, 只能接收数据, 不能发送数据
// 在close之前如果dup/dup2-> 文件描述符复制了, 原来的关闭了, 复制出的fd还能双向通信
// 半关闭函数
#include <sys/socket.h>
int shutdown(int sockfd, int how);
参数:
- sockfd: 要操作的文件描述符
- how: 操作方式
- SHUT_RD: 关闭读
- SHUT_WR: 关闭写
- SHUT_RDWR: 关闭读写
1.查看网络相关信息的命令
$ netstat
○ 参数:
-a (all)显示所有选项,默认不显示LISTEN相关
-p 显示建立相关链接的程序名
-n 拒绝显示别名,能显示数字的全部转化成数字。
-l 仅列出有在 Listen (监听) 的服务状态
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
robin@OS:~$ netstat -apn|grep 9999
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 1401/server
robin@OS:~$ netstat -apn|grep 9999
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 1401/server
tcp 0 0 127.0.0.1:37030 127.0.0.1:9999 ESTABLISHED 1422/client
tcp 0 0 127.0.0.1:9999 127.0.0.1:37030 ESTABLISHED 1401/server
robin@OS:~$ netstat -apn|grep 9999
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 1 0 127.0.0.1:37030 127.0.0.1:9999 CLOSE_WAIT 1422/client
tcp 0 0 127.0.0.1:9999 127.0.0.1:37030 FIN_WAIT2 -
robin@OS:~$ netstat -apn|grep 9999
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:9999 127.0.0.1:37030 TIME_WAIT -