赞
踩
网络编程系列文章:
第1章 Linux系统编程入门(上)
第1章 Linux系统编程入门(下)
第2章 Linux多进程开发(上)
第2章 Linux多进程开发(下)
第3章 Linux多线程开发
第4章 Linux网络编程
TCP 是一种 面向连接的 单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“ 连接”,其实是 客户端 和 服务器 的内存里保存的一份关于对方的信息,如 IP 地址
、端口号
等。
TCP 可以看成是一种字节流,它会处理 IP 层 或 以下的层 的 丢包
、重复
以及 错误问题
。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。
TCP 提供了一种 可靠
、面向连接
、字节流
、传输层的服务
,采用 三次握手 建立一个连接。采用 四次挥手 来关闭一个连接。
三次握手 的目的是保证双方互相之间 建立了连接。三次握手 发生在 客户端连接 的时候,当调用
connect()
,底层会通过TCP协议进行 三次握手。
三次握手:
TCP 三次握手状态转换(不携带数据):
第一次握手:
SYN
标志位 置为 1
seq=J
, 这个序号后边是可以携带数据(数据的大小)第二次握手:
ACK=1
ack
= 客户端的序号
+ 数据长度
+ SYN
/FIN
(按 一个字节 算)SYN=1
seq = K
第三次握手:
ACK=1
ack
= 服务端的序号
+ 数据长度
+ SYN
/ FIN
(按 一个字节 算)为什么要三次握手,而不是两次握手:确认客户端 和 服务器 端都能 收发数据,以保证建立的 连接是可靠的。
TCP 头部结构
16 位端口号(port number
):告知主机报文段是来自哪里(源端口)以及传给哪个上层协或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统 自动选择 的 临时端口号。
32 位序号(sequence number
):一次 TCP
通信(从 TCP
连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。⭐️
TCP 报文段
中,序号值被系统初始化为某个 随机值 ISN(Initial Sequence Number
,初始序号值)。那么在该传输方向上(从 A
-> B
),后续的 TCP 报文段中 序号值 将被系统设置成 ISN
加上 该报文段所携带数据的 第一个字节在整个字节流中的偏移
。
例如,某个 TCP 报文段传送的数据是字节流中的第
1025 ~ 2048
字节,那么该报文段的序号值就是ISN + 1025
。另外一个传输方向(从 B 到 A)的 TCP 报文段的序号值也具有相同的含义。
32 位确认号(acknowledgement number
):用作对另一方发送来的 TCP 报文段
的响应。其值是收到的 TCP 报文段的序号值
+ 数据长度
/ (数据长度
+1
)。⭐️
TCP 报文段
不仅携带自己的序号,而且包含对 B 发送来 的 TCP 报文段
的 确认号。TCP 报文段
也同样携带自己的序号 和 对 A 发送来的 报文段
的确认序号。注意:只有收到 标志位
SYN=1
或FIN=1
时,确认序号ack
才会再+1
;其余的都是加上具体数据长度
。
4 位头部长度(head length
):标识该 TCP 头部有多少个 32 bit
(4 字节
)。因为 4 位最大能表示 15
,所以 TCP 头部最长是 60 字节
。
6 位标志位 包含如下几项:
URG
标志,表示 紧急指针(urgent pointer
)是否有效。ACK
标志,表示 确认号 是否有效。我们称携带 ACK
标志的 TCP 报文段为 确认报文段
。⭐️PSH
标志,提示接收端应用程序应该 立即 从 TCP 接收缓冲区 中 读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。RST
标志,表示要求对方 重新建立连接。我们称携带 RST
标志的 TCP 报文段为 复位报文段
。SYN
标志,表示 请求建立一个连接。我们称携带 SYN
标志的 TCP 报文段为 同步报文段
。⭐️FIN
标志,表示通知对方本端要 关闭连接了。我们称携带 FIN
标志的 TCP 报文段为 结束报文段
。⭐️16 位窗口大小(window size
):是 TCP 流量控制的一个手段。这里说的窗口,指的是 接收通告窗口(Receiver Window
,RWND)。它告诉对方本端的 TCP 接收缓冲区 还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。⭐️
16 位校验和(TCP checksum
):由发送端填充,接收端对 TCP 报文段执行 CRC 算法
以校验TCP 报文段在传输过程中是否损坏。注意,这个校验 不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。
16 位紧急指针(urgent pointer
):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为 紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
滑动窗口(Sliding window)是一种 流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。
TCP 中采用 滑动窗口 来进行 传输控制,滑动窗口的大小意味着 接收方 还有多大的 缓冲区 可以用于接收数据。发送方 可以通过滑动窗口的大小来确定 应该发送多少字节的数据。当滑动窗口为 0
时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认
、流量控制
、拥塞控制
的承载结构。
窗口 理解为 缓冲区的大小
- 滑动窗口的大小会随着发送数据和接收数据而变化。
- 通信的双方都有 发送缓冲区 和 接收数据的缓冲区
服务器:
- 发送缓冲区(发送缓冲区的窗口)
- 接收缓冲区(接收缓冲区的窗口)
客户端:
- 发送缓冲区(发送缓冲区的窗口)
- 接收缓冲区(接收缓冲区的窗口)
发送方的缓冲区:
接收方的缓冲区:
举例:
# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 滑动窗口
1. 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3. 第三次握手
4. 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
5. 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6. 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7. 第12次,客户端给服务器发送了1k的数据
8. 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9. 第14次,服务器回复ACK 8194, a:同意断开连接的请求 b:告诉客户端已经接受到方才发的2k的数据 c:滑动窗口2k
10.第15、16次,通知客户端滑动窗口的大小
11.第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.第18次,第四次回收,客户端同意了服务器端的断开请求
四次挥手 发生在 断开连接 的时候,在程序中当调用了 close()
会使用TCP协议进行四次挥手。
客户端和服务器端 都可以主动发起断开连接,谁先调用 close()
谁就是发起。
因为在TCP连接的时候,采用 三次握手 建立的的 连接是双向的,在断开的时候需要 双向断开。
四次挥手状态转换:
要实现TCP通信服务器 处理 并发的任务,使用 多线程 或者 多进程 来解决。
(1)多进程思路:
⭐️ 服务器端(server_process.c
)
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recyleChild(int arg) { // 使用信号释放子进程资源
while(1) { // 处理同时结束多个子进程的情况
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
}else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0){
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask); // 清空临时阻塞信号掩码
act.sa_handler = recyleChild; // 回调函数
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);
// 1. 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 2. 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3. 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不断循环等待客户端连接
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 4. 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) { // 产生中断
continue;
}
perror("accept");
exit(-1);
}
// 5. 每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) { // 先不考虑错误的情况
// 子进程
// 获取客户端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}else{
close(cfd);
}
}
// 6. 关闭文件描述符
close(lfd);
return 0;
}
⭐️ 客户端(client.c
)
// TCP通信的客户端
#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.216.129", &serveraddr.sin_addr.s_addr);
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];
int i = 0;
while(1) {
sprintf(recvBuf, "data : %d\n", i++);
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1); // 加上字符串结束符
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
sleep(1);
}
// 关闭连接
close(fd);
return 0;
}
(2)多线程实现并发服务器:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
// 把所需要参数放到一个结构体中
struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr; // 客户端信息
pthread_t tid; // 线程号
};
struct sockInfo sockinfos[128]; // 可以支持128个客户端同时连接
void * working(void * arg) {
// 子线程和客户端通信 cfd 客户端的信息 线程号
// 获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;
char cliIp[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}
int main() {
// 1. 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 2. 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3. 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 4. 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
struct sockInfo * pinfo;
for(int i = 0; i < max; i++) {
// 从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i--;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);
// 5. 创建子线程,通信
pthread_create(&pinfo->tid, NULL, working, pinfo);
// 设置线程分离,子线程结束后,自动回收资源
pthread_detach(pinfo->tid);
}
// 6. 关闭文件描述符
close(lfd);
return 0;
}
黑色实线:异常(可以先不看)
红色实现:客户端
绿色虚线:服务器
FIN
和最终的 ACK
后,连接的主动关闭方必须处于 TIME_WAIT
状态并持续 2MSL
时间。ACK
丢失 的情况下 重新发送最终的 ACK
。ACK
并不是因为 被动关闭方 重传了 ACK
(它们并不消耗序列号,被动关闭方也不会重传),而是因为 被动关闭方 重传了它的 FIN
。事实上,被动关闭方总是重传 FIN
直到它收到一个最终的 ACK
。主动断开 连接的一方, 最后进出入一个
TIME_WAIT
状态, 这个状态会持续:2msl
msl
: 官方建议:2分钟
, Linux中实际是30s
半关闭
当 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);
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)
后,其它的进程将无法进行通信。但如果一个进程close(sfd)
将 不会影响 到其它进程。
端口复用 最常用的用途是:
FIN_WAIT_2
或 TIME_WAIT
状态时,其端口号被占用,还未释放,不能再次启动服务器##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 (允许重用本地地址)
- SO_REUSEPORT (允许重用本地端口)
- optval : 端口复用的值(整形 / 结构体)
- 1 : 可以复用
- 0 : 不可以复用
- optlen : optval参数的大小
// 端口复用,设置的时机是在服务器绑定端口之前。
setsockopt();
bind();
常看 网络相关信息 的命令 netstat -anp
-a
所有的 socket
-p
显示正在使用 socket
的程序的名称-n
直接使用IP地址,而不通过域名服务器注:仅供学习参考,如有不足,欢迎指正!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。