TCP通信(单进程/多进程/多线程/线程池)

TCP通信

本节代码详见https://gitee.com/hepburn0504-yyq/linux-class/tree/master/2023_04_16_TCPSocket

与UDP相比较,UDP服务端只需要调用socket函数创建套接字+bind绑定就完事了;而TCP服务端不仅要做socket和bind,还要listen和accept。

recvfrom是面向数据报的收发数据函数,仅适合UDP。TCP收发数据有两套接口可以用(read和write、send和)

因为TCP是面向连接的,所以要用listen和accept。

头文件4件套与UDP相同。

TCP是面向字节流的,读取数据有可能会不按发送格式读取,上层也不知道我这次调用read,是读到了几个TCP数据包,可能是一个,半个,也可能是好几个;而UDP是面向数据报的,每次recvfrom,一定是读的一个UDP数据包,即使留的buffer太小没读全,下次recvfrom也是读下一个数据包,前一个数据包未读到的部分就丢了。

函数

socket函数、bind函数、bzero函数与udp的使用差不多。

listen函数

功能:让TCP服务端进入监听状态
头文件
    #include <sys/types.h>
    #include <sys/socket.h>
原型
	int listen(int sockfd, int backlog);
参数:
    sockfd: 流式套接字(即文件描述符),传入socket函数的返回值
    backlog: 全队列连接长度
返回值:
    成功返回0,失败返回-1,会设置errno

最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略。

accept函数

三次握手完成后,服务器调用accept()接受连接;如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。如果给addr 参数传NULL,表示不关心客户端的地址。

功能:让TCP服务端获取新连接,获取真正用于服务的套接字
头文件
    #include <sys/types.h>
    #include <sys/socket.h>
原型
	int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:
    sockfd: 套接字(即文件描述符),传入socket函数的返回值
    src_addr:输出型参数,用于存储对方的ip和port
	addrlen:输入输出型参数,输入的是src_addr的大小,输出实际读到的字节数
返回值:
    成功返回套接字文件描述符fd,失败返回-1,会设置errno
accept返回值server_sock vs socket返回值listen_sock

该函数的返回值也是一个套接字(文件描述符),和socket函数的返回值有什么区别?

listen_sock 的功能是帮助accept函数建立底层连接(可以通过listen_sock 来获取新连接),往后真正进行服务的是accept函数的返回值server_sock 。

connect函数

客户端需要调用connect()连接服务器

头文件
	#include <sys/types.h>
    #include <sys/socket.h>
功能:客户端通过此函数与服务端建立连接
原型
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
    sockfd:套接字(即文件描述符),传入socket函数的返回值
	buf:自己设定的缓冲区
	len:sizeof(buf),缓冲区的最大字节数
	flags:0表示以阻塞方式读取
	src_addr:输出型参数,用于存储对方的ip和port
	addrlen:输入输出型参数,输入的是src_addr的大小,输出实际读到的字节数
返回值
    成功返回0,失败返回-1,errno可以查看出错信息。

connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。

ip地址转换函数

点分十进制字符串且为主机序列 转 sockaddr_in结构体且为网络字节序

头文件
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
函数原型
    int inet_aton(const char *cp, struct in_addr *inp);
    in_addr_t inet_addr(const char *cp);

头文件
    #include <arpa/inet.h>
函数原型
    int inet_pton(int af, const char *src, void *dst);

sockaddr_in结构体且为网络字节序 转 点分十进制字符串且为主机序列

头文件
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
函数原型
    char *inet_ntoa(struct in_addr in);

头文件
    #include <arpa/inet.h>
函数原型
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块static的内存来保存ip的结果,inet_ntoa函数, 是把这个返回结果放到了静态存储区。但是这不需要调用者手动释放。该函数有静态成员会有线程安全的问题,即当多次调用时,第二次调用该函数时的结果会覆盖掉上一次的结果。

在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

read函数

头文件
	#include <unistd.h>
功能:读取数据
原型
	ssize_t read(int fd, void *buf, size_t count);
参数
    fd:套接字(即文件描述符),传入socket函数的返回值
	buf:自己设定的缓冲区
	count:sizeof(buf),缓冲区的最大字节数

read的返回值有3种情况:

  1. >0 两端都处于正常连接状态;
  2. ==0 表示对端关闭连接;
  3. <0 表示读取数据失败

write函数

功能:写入数据
头文件
    #include <unistd.h>
原型
    ssize_t write(int fd, const void *buf, size_t count);
参数
    fd:套接字(即文件描述符),传入socket函数的返回值
	buf:自己设定的缓冲区
	count:sizeof(buf),缓冲区的最大字节数

send函数

头文件
	#include <sys/types.h>
    #include <sys/socket.h>
功能:发送数据
原型
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数
    sockfd:套接字(即文件描述符),传入socket函数的返回值
	buf:自己设定的缓冲区
	len:sizeof(buf),缓冲区的最大字节数
	flags:0表示以阻塞方式读取

返回值与read函数的3种情况相同。

recv函数

头文件
	#include <sys/types.h>
    #include <sys/socket.h>
功能:发送数据
原型
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数
    sockfd:套接字(即文件描述符),传入socket函数的返回值
	buf:自己设定的缓冲区
	len:sizeof(buf),缓冲区的最大字节数
	flags:0表示以阻塞方式读取

命令

netstat

功能:显示以指定方式连接当前主机的网络通信

参数 -n 把收到的消息尽量显示为数字 -t 显示tcp形式的连接 -p 以进程形式显示 -a全部显示 -l处于listen状态的TCP服务端口

127.0.0.1 是本地环回,即两个进程用此ip进行网络通信的时候,数据只会在本地协议栈中流动,不会把数据发送到网络中。所以通常用于本地网络服务器测试。

netstat -ntpl #查看处于listen状态的TCP服务器进程

客户端实现细节

客户端建立连接过程如下

  1. 调用socket, 创建文件描述符;
  2. 调用connect, 向服务器发起连接请求;
  3. connect会发出SYN段并阻塞等待服务器应答; (第一次)
  4. 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  5. 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

客户端无需显示bind,也不用listen和accept。但是一定需要connect自己的port。

服务端实现版本

初始化流程如下

  1. 调用socket, 创建文件描述符;
  2. 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  3. 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  4. 调用accecpt, 并阻塞, 等待客户端连接过来;

服务端的端口号一旦确定,就不可轻易更改。

版本1:单进程循环监听

因为我们accecpt了一个请求之后,就在一直while循环尝试read和write,没有继续调用到accecpt,导致不能接受新的请求。一次只能处理一个客户端的数据,只有当前与服务端相连的客户端退出以后,才能和第二个客户端建立新的连接!

版本2:多进程+忽略SIGCHLD信号

思考:1.子进程能否打开父进程曾经创建的文件描述符呢? 2.如果父进程关闭server_sock,对子进程有影响吗?

1、子进程能否打开父进程曾经创建的文件描述符呢?可以,子进程继承父进程的数据,每个进程各自有一张文件描述符表,能看到之前的listen_sock和server_sock。所以子进程需要关闭掉自己不需要的套接字listen_sock,父进程必须关闭掉server_sock。如果父进程不关listen_sock,如果不小心修改了listen_sock,会导致服务器异常退出;如果父进程不关server_sock,就会导致父进程可用的文件描述符越来越少(一个进程能打开的 文件描述符是有限的),造成文件描述符泄漏

2、子进程服务结束就会退出,变成僵尸状态,这个时候父进程一定要waitpid回收子进程的退出。但是waitpid函数一般都是阻塞式等待,如果使用 wait ,会导致父进程不能快速再次调用到 accept,仍然没法处理多个请求那就与单进程版本无异。(非阻塞等待比较麻烦,需要保存所有子进程的id,循环式遍历)

解决方案:由于子进程退出会给父进程发送SIGCHLD信号,那么我们就通过主动忽略SIGCHLD信号让子进程退出的时候自动释放自己的僵尸状态

pid_t id = fork();
assert(id != -1);
if (id == 0)
{
	//子进程继承父进程的资源数据,能看到_listen_sock和server_sock
	close(_listen_sock);//子进程关掉自己不需要的套接字
	// 子进程服务
	service(server_sock, client_ip, client_port);
	exit(0); // 子进程会变成僵尸状态
}
close(server_sock);//父进程关掉自己不需要的套接字

版本3:多进程+子进程再fork

即真正为我们提供服务的是孙子进程,由于子进程退出了,那么孙子进程就变成了孤儿进程,会被OS领养,即init 1号进程会对其进行管理和资源回收。

由于子进程立马退出了,那么父进程也可以直接waitpid返回,不会阻塞等待。

pid_t id  = fork();
if(id == 0)
{
    close(_listen_sock);
    //子进程
    if(fork() > 0) exit(0);//让子进程本身退出
    //让子进程的子进程就会托孤给init 1号进程
    service(server_sock, client_ip, client_port);
    exit(0);
}
waitpid(id, nullptr, 0);
close(server_sock);

版本4:多线程+pthread_detach

每次有一个新的连接请求,就创建一个线程来处理。

在多线程的情况下,不用关闭特定的socket文件描述符。因为线程与主线程共享一张文件描述符表。

线程一定要进行join,否则会造成内存泄漏。

解决方案:可以在每个线程的执行函数内部执行pthread_detach,进行线程分离,就无需阻塞join等待。

版本5:多线程+线程池

一般用线程池的服务器,处理业务要尽量避免常连接,比如服务器检测到在一段时间内无操作的情况下主动关闭连接。(如果从建立连接到断开连接,要一直保持连接->常连接)。

功能:大小写转换、英汉互译等

telnet工具

sudo yum install telnet -y #安装telnet工具
telnet 127.0.0.1 8080 #远程登陆工具telnet

下面是一些常用的telnet命令用法:

  1. 连接到远程主机:telnet <远程主机IP地址> [端口号] 例如:telnet 127.0.0.1 8080 这个命令会连接到IP地址为127.0.0.1的主机的8080端口.

  2. 发送命令:在连接上远程主机后,可以输入命令来执行操作。输入Ctrl+]命令,再回车,可以进入发送消息的功能。

  3. 退出连接:先输入Ctrl+]命令退出发送消息的功能,再输入quit,可以退出当前的telnet连接。

需要注意的是,telnet命令在现代网络中已经不太安全,因为它的数据传输是明文的,容易被黑客截获。因此,建议使用更加安全的SSH协议来进行远程登录。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值