tcp通信流程:面向连接,可靠传输,面向字节流
客户端(client):
- 创建套接字
- 绑定地址信息(不推荐主动)
- 向服务端发出连接请求
- 收 / 发数据
- 关闭套接字
服务端(server):
- 创建套接字——在内核创建socket结构体与网卡建立联系
- 为套接字绑定地址信息——告诉操作系统哪些数据交给我处理—放到我的接收缓冲区
- 开始监听——告诉操作系统可以开始接收哭护短的连接请求—客户端与服务端进行tcp通信必须首先建立连接—确保双方都具有数据收发的能力。
当服务端收到了客户端的连接请求,连接的建立过程是在内核完成的。
tcp服务端最早的套接字只用于接收新连接请求,连接请求到来之后创建新的套接字,创建的新的套接字用于与客户端进行后续的通信 - 服务端程序获取新创建的socket套接字操作句柄
服务端程序在内核会接收新连接,创建新的socket
服务端程序获取内核中新创建的socket操作句柄
在服务端程序这边通过这个获取的操作句柄与客户端进行通信 - 服务端通过新建套接字可以与客户端进行数据通信
接收数据 / 发送数据
tcp在数据通信的时候,谁先发送都可以(因为这时候连接已经建立) - 关闭套接字:释放资源
socket进行tcp通信的接口介绍:
1、创建套接字
int socket(int domain,int type,int protocol)
(地址域 AF_INET,套接字类型 SOCK_STREAM,协议类型 IPPROTO_TCP)
2、绑定地址信息
int bind(int sockfd,sockaddr *addr,socklent_len);
3、开始监听
int listen(int sockfd,int backlog);
backlog:内核中tcp连接pending队列的最大节点数——决定了同一时刻服务端所能接收的客户端连接请求数量
网络攻击:SYN泛洪攻击:
恶意主机不断向服务端发送大量的建立连接请求,若服务端依然为每一个请求建立一个socket,一瞬间就资源耗尽服务器崩溃
4、服务端程序获取新创建的socket操作句柄
int accept(int sockfd;struct sockaddr *addr,socklen_t len);
这一步的功能就是从内核中的已完成连接队列中获取socket,腾出一个位子可以继续接收新连接
sockfd:描述符——指定获取哪个服务端socket对于新连接所创建的新套接字socket,是那个监听套接字–队列也是监听套接字的队列
addr:客户端的地址信息
len:输入输出型参数:指定想要获取的地址信息长度,以及获取实际得到的地址信息长度
返回值:正确返回具体新创建套接字的操作句柄——文件描述符——用于后续与指定客户端进行通信; 失败返回-1
5、通过新获取的套接字描述符进行通信
//接收数据
//成功返回实际接收字节数;失败返回-1,若连接断开则返回0
int recv(int sockfd,char *buf,int len,int flag);
//发送数据
//成功返回实际发送字节数;失败返回-1;连接断开则触发异常
int send(int sockfd,char *data,int len,int flag);
因为tcp通信的套接字socket中描述的不仅有源端信息,也有对端信息,因此recv / send不需要再去由各自程序指定对端地址了。
sockfd:对于服务端来说就是accept返回的新创建的套接字描述符
6、关闭套接字
int close(int fd);
7、客户端向服务端发起连接
int connect(int sockfd,struct sockaddr *srvaddr,socklen_t len);
sockfd:客户端套接字——若没有绑定源端地址,则操作系统会自动绑定,同时会将服务端地址信息描述进socket结构体中
srvaddr:服务端的地址信息
基本tcp服务端程序存在的问题
服务端流程阻塞:
1、服务端处理完第一个客户端的数据收发之后,循环又调用accept,因为没有更多连接到来,因此流程阻塞在这里,等待新连接到来
2、服务端程序接收了第一个客户端连接请求,走到等待接收第一个客户端的数据处Recv;但是第一个客户端如果不发送数据,程序流程就阻塞在这里,就算有后续其他连接,也不会被处理。
归根结底,服务端流程会阻塞,是因为程序并不知道什么是时候会有新连接到来也不知道什么时候数据会到来,因此只能固定将程序流程写为先获取连接,再接收数据(与客户端通信);然而获取连接以及与客户端通信都有可能会阻塞;
因此得到结论:服务端程序流程的阻塞是因为服务端的代码不够灵活;
当前解决方案:多执行流解决方案
服务端程序中主要有两个功能并且都有可能阻塞:获取新连接 / 与每个客户端进行通信
若主执行流只负责获取新连接;新连接获取之后,为与这个客户端进行通信的功能创建一个执行流
创建新的执行流进行与客户端的通信
没有新连接到来,则只是主执行流阻塞;但是与客户端通信的执行流依然可以进行通信
而若与客户端通信的执行阻塞,主执行流依然可以获取新连接,创建新的执行流与另一个客户端进行通信
总结:
服务端无法与客户端持续进行通信
主要原因:服务端流程固定,在一个流程中要完成多种功能,并且每种功能接口都有可能阻塞
解决方案:采用多执行流方案,每个执行流只负责一个功能,一个执行流阻塞也不会影响其他执行流的操作
具体技术:多进程 / 多线程
1、多进程任务处理:
父进程仅仅获取新建连接,若获取了新建连接,则为新建连接创建一个子进程来负责这个新建连接与指定客户端的通信
(1)、父子进程代码共享,但是数据都有;父子进程都有新建套接字描述符;然而父进程并不与客户端进行通信,因此一定要关闭新建socket
(2)、父进程要注意避免僵尸进程的产生,因此在信号回调函数中进行进程等待
注意避免僵尸子进程:SIGCHLD信号自定义处理;父子进程各自都有一个newsock;父进程不使用要及时关闭防止资源泄露
2、多线程任务处理:
主线程仅仅获取新建连接,若获取了新建连接,则创建一个新线程来负责通过这个新建连接与客户端进行通信
(1)、多线程不像多进程,可以直接通过复制拥有父进程的所有数据;在线程入口函数中,想要通过新建套接字进行通信,必须通过函数传参的形式将新建套接字传入线程入口函数中进行操作。
传参的时候不能直接传递局部变量的地址——因为循环完毕局部变量会被释放
①、直接不适用局部变量,而是直接在堆上申请一块空间,将空间首地址传递给线程
TcpSocket *newsock = new TcpSocket();
②、直接将描述符的值通过传参传入
(2)、多线程之间资源是共享进程的,意味着这个新建套接字是只有一份的,虽然这个套接字交给线程进行处理了,但是主线程不能随意关闭新建套接字(线程间共享文件描述符表)
来源:CSDN
作者:blank
链接:https://blog.csdn.net/weixin_43867777/article/details/104920377