【Linux网络】UDP套接字【实现英汉转化】
一:Udp Server服务端
先看Udp Server服务端的整体框架:
Log lg;enum{SOCKET_ERR = 1,BIND_ERR
};class UdpServer
{
public:UdpServer(uint16_t port):port_(port){}~UdpServer(){}
public:
private:int sockfd_; // 网络文件描述符std::string ip_; // IPuint16_t port_; // 表明服务器进程的端口号bool isrunning; // 服务器是否运行
};
1.1、socket—创建套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
- 第一个参数 domain:协议域,决定了 socket 套接字的地址类型,在通信时必须采用相应的地址。例如 AF_INET 决定了采用 IPv4 地址(32位)与端口号(16位)的组合;AF_INET6 决定了采用 IPv6 地址(128位)与端口号(16位)的组合;AF_UNIX 决定了采用一个绝对路径名作为地址。
- 第二个参数 type:指定 socket 套接字的类型,是 SOCK_STREAM(流式套接字)还是 SOCK_DGRAM(数据报式套接字)等。
- 第三个参数 protocol: 协议字段,表明要指定的协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)等等。
- 返回值:返回一个文件描述符,因为创建套接字的本质就是打开一个文件。
void Init(){// 1.创建udp socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);}
1.2、bind—将套接字与特定IP和特定端口号port进行绑定
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第一个参数 sockfd:socket 函数的返回值,一个文件描述符。
第二个参数 addr:要传输的套接字种类,取地址之后需要强转成统一套接字(struct sosckaddr*)
第三个参数 addrlen:传输的套接字的大小
void Init(){// 1.创建udp socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2.绑定端口号struct sockaddr_in local;bzero(&local, sizeof(local)); // 将local内部清零// 填充 socket套接字相关字段local.sin_family = AF_INET; // 当前结构体的地址类型local.sin_port = htons(port_); // 当前服务器的端口号,须将主机序列转换成网络序列local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 网络中的ip序列是4字节传输的,所以将字符串风格的ip地址转换成可以在网络中传输的4字节Ip网络序列// 开始绑定,bind本质就是将上面一系列参数设置进内核,到指定的套接字中int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));if(n < 0){lg(Fatal, "bind error, error: %d, err message: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err message: %s", errno, strerror(errno));}
要网络通信, 就要使用到 socket套接字,那么首先就必须定义一个 struct sockaddr_in 类型的结构体对象,该对象中有4个字段:sin_family 字段表示当前结构体的地址类型,sin_port 字段表示端口号,sin_addr 字段表示 IP 地址,sin_zero 字段表示填充。其中 sin_zero 字段一般我们不做处理,只处理其他三个字段即可。首先 sin_family 字段必须和 socket 函数中的 domain 字段保持一致,因为是在网络中通信传输的,所以 sin_port 和 sin_addr 必须将我们当前的 port 和 ip 转换成可以在网络中通信的序列,即使用 htons 函数将端口号从主机转成网络序列,使用 inet_addr 函数将 我们平常方便查看的字符串 ip 情况转成可以在网络中传输的 4 字节的 ip 情况。而 sin_addr 中只有 s_addr 字段,最总就是将该字段填充即可。
所以,所谓 bind,本质就是将我们的套接字与一个特定的 ip 和一个特定的端口号 port 相关联,这样就能与世界上的特定主机上的某一个进程相互连通。
1.3、recvfrom—从服务器的套接字中读取数据
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- 第一个参数 sockfd:表示当前被读取数据的服务器的套接字
- 第二个参数 buf:表示要接收数据的缓冲区
- 第三个参数 len:要接收的缓冲区的大小
- 第四个参数 flags:默认设为0,表示阻塞
- 第五个参数 src_addr:输出型参数,表示获取客户端的套接字信息,因为我们是从服务器的套接字读取数据的,读取的数据给谁呢?毫无疑问是给指定的客户端,所以就要获取到客户端的 ip 和端口号。因为是 UDP 网络通信,所以传的是 struct sockaddr_in 类型的对象地址,并将其强转。
- 第六个参数 addrlen:struct sockaddr_in 对象的大小
- 返回值:成功返回获取到数据的字节数,失败返回 -1
void Run(){isrunning = true;char inbuffer[SIZE];while (isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));continue;}// 此时服务端的数据,我们已经拿到了并将其存至了 inbuffer里面inbuffer[n] = 0; //在结尾添加'\0'std::string info = inbuffer; std::string echo_string = "server say@ " + info;}}
1.4、sendto—向指定套接字中发送数据
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 第一个参数 sockfd:表示当前被读取数据的服务器的套接字
- 第二个参数 buf:要发送的数据缓冲区
- 第三个参数 len:发送的数据缓冲区的大小
- 第四个参数 flags:默认设为0
- 第五个参数 src_addr:接收方的套接字信息。这里的接收方是客户端
- 第六个参数 addrlen:struct sockaddr_in 对象的大小
- 返回值:成功返回获取到数据的字节数,失败返回 -1
void Run(){isrunning = true;char inbuffer[SIZE];while (isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));continue;}// 此时服务端的数据,我们已经拿到了并将其存至了 inbuffer里面inbuffer[n] = 0; //在结尾添加'\0'std::string info = inbuffer; std::string echo_string = "server say@ " + info;// 向客户端发送信息ssize_t r = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);if(r < 0){lg(Warning, "sendto error, errno: %d, err message: %s", errno, strerror(errno));continue;}}}
netstat -nlup 【net表示网络,stat表示状态,n 表示将所有能显示成数字的信息都以数字的形式显示出来,u表示udp,p 表示PID信息】测试一下:
注意:对于网络信息的发送和接收,我们不需要自己来进行主机转网络,网络转主机来转换的。数据内容会自动由 recvfrom 和 sendto 函数来转换解决。
1.5、绑定 IP 与端口号
云服务器禁止直接 bind 绑定公网 IP
一般建议服务端代码在 ip 绑定时使用 0,0,0,0,表示任意地址绑定。因为这样只要是发送到这台主机的数据信息,我们的这个服务器进程都能收到,在根据端口号向上交付。且一个服务器可能含有多个 ip,所以如果服务端的进程只绑定一个固定的 ip 的话,那么通过其他 ip 发送到这个服务器的数据,这个进程就无法收到该信息。所以,我们一般在设置服务器 ip 时通常设置服务端套接字的 local.sin_addr 字段:
local.sin_addr.s_addr = 0
// 或者
local.sin_addr.s_addr = INADDR_ANY
本地环回地址 IP(通常用来cs)
本地环回地址:127.0.0.1。
任何服务器都可以绑定 127.0.0.1 这个 ip 地址,绑定了这个地址后,该进程不会向网络中发送数据,但还是会通过网络协议栈,通常用来进程本主机的测试。
端口号的绑定
服务端的端口在被绑定时,不能想绑定哪个就绑定哪个。一般端口号 [0,1023] 是系统内定的端口号,有其自己固定的应用层协议使用。例如:http 的端口号是80,https 的端口号是443等等。端口号的选择一般范围在 [1024,65535] 之间。
1.6、Udp Server服务端总代码
// UdpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <string.h>
#include <strings.h> // bzero
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>#include "Log.hpp"
Log lg;uint16_t defaultport = 8888;
std::string defaultip = "0.0.0.0";
using func_t = std::function<std::string(const std::string&)>;enum{SOCKET_ERR = 1,BIND_ERR
};class UdpServer
{
public:UdpServer(uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip), isrunning(false){}~UdpServer(){if(sockfd_ > 0)close(sockfd_);}
public:void Init(){// 1.创建udp socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2.绑定端口号struct sockaddr_in local;bzero(&local, sizeof(local)); // 将local内部清零// 填充 socket套接字相关字段local.sin_family = AF_INET; // 当前结构体的地址类型local.sin_port = htons(port_); // 当前服务器的端口号,须将主机序列转换成网络序列local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 网络中的ip序列是4字节传输的,所以将字符串风格的ip地址转换成可以在网络中传输的4字节Ip网络序列// 开始绑定,bind本质就是将上面一系列参数设置进内核,到指定的套接字中int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));if(n < 0){lg(Fatal, "bind error, error: %d, err message: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err message: %s", errno, strerror(errno));}void Run(func_t func) // 让服务器执行特定的函数功能{isrunning = true;char inbuffer[SIZE];while (isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));continue;}// 此时服务端的数据,我们已经拿到了并将其存至了 inbuffer里面inbuffer[n] = 0; //在结尾添加'\0'std::string info = inbuffer; // std::string echo_string = "server say@ " + info;std::string echo_string = func(info);// 向客户端发送信息ssize_t r = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);if(r < 0){lg(Warning, "sendto error, errno: %d, err message: %s", errno, strerror(errno));continue;}}}
private:int sockfd_; // 网络文件描述符std::string ip_; // IPuint16_t port_; // 表明服务器进程的端口号bool isrunning; // 服务器是否运行
};
// UdpServer.cc#include <iostream>
#include <string>
#include "UdpServer.hpp"void Usage(std::string proc)
{std::cout << "proc: " << proc << ", port[1024+]" << std::endl;
}std::string Handler(const std::string& str)
{ std::string res = "Server a message: ";res += str;std::cout << res << std::endl;return res;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);UdpServer* usvr = new UdpServer(port);usvr->Init();usvr->Run(Handler);return 0;
}
二:Udp Client客户端
一个端口号只能被一个进程bind,对 server 是如此,对 client 也是。所以客户端也要绑定端口号,只不过不需要用户显示的绑定,而是由操作系统自由随机选择。这样可以避免端口号发生冲突。其次其实对客户端来讲,端口号是多少并不重要,只要能够保证该进程在主机上的唯一性就可以。因为一般都是客户端主动的向服务端发送信息数据。所以客户端一定要能知道服务端的端口号。相反服务端的端口号是确定的。所以在编写客户端代码时,无需进行绑定端口号,直接往服务器中发送数据。在 UDP 首次发送数据的时候,系统会自动地为我们动态bind绑定。
// UdpClient.cc#include "Log.hpp"
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>using namespace std;
Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << ", serverip serverport" << std::endl;
}// ./udpclient serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}// 获取 serverip 和 serverportstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 建立套接字,向服务器当中发信息struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cout << "socket error" << std::endl;return 1;}// 套接字创建完毕,直接就可以发信息了std::string message;char buffer[1024];while(true){std::cout << "Please Enter@ ";getline(cin, message); // 要发送给服务端的数据// 数据有了,给谁发?已经有了服务端的结构体信息了,那就给它发sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);// 只要给服务器发了数据,服务器就会很快的识别到并作出处理// 接收服务器数据struct sockaddr_in temp; //从服务器获取下来给我们应答的数据socklen_t tlen = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &tlen);if(s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}close(sockfd);return 0;
}
三:指令处理
因为服务端收到来自客户端的数据后,会对数据进行加工处理,之后再返回给客户端。那么我们就可以将对数据处理的方法独立出来,作为一个函数来传给服务端的 Run 方法。这样就大大提高了代码的维护性。例如,客户端输入命令,服务端接收到该命令,并将执行结果返还给客户端。
看一个函数 popen:
#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
因为对于指令的执行,需要单独新起一个进程,即需要 fork 创建子进程。而 popen 函数不仅仅能将一个文件打开,还可以调用 fork 函数创建子进程,让子进程进行程序替换执行对应的命令。
- 参数 command:表示要执行的命令
- 参数 type:" r "表示读取数据," w "表示写入数据。
- 返回值:成功返回文件指针,失败返回 NULL,错误原因存至 errno 全局变量中。
所以,只需要将要执行的方法函数做为参数传进 Run 中即可。
// 指令处理
bool SafeCheck(const std::string &cmd)
{std::vector<std::string> v = {"mv", "cp", "git", "su", "top"};for(auto& e : v){if(cmd.find(e) != std::string::npos) // 找到啦对应的指令{return false;}}return true;
}std::string ExcuteCommand(const std::string& cmd)
{if(!SafeCheck(cmd)) {return "The command is prohibited";}FILE* fp = popen(cmd.c_str(), "r");if(fp == nullptr){perror("popen");return "error";}// 读取执行的结果std::string result;char buffer[4096];while (true){char* ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr)break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);// UdpServer* usvr = new UdpServer(port);std::unique_ptr<UdpServer> usvr(new UdpServer(port));usvr->Init();// usvr->Run(Handler);usvr->Run(ExcuteCommand);return 0;
}
四:基于 Udp 套接字的群聊
4.1、Server 服务端
因为是群聊,那么聊天的人就不只是一两个人。即再群聊中发送的信息是会被群聊中的所有用户所看到的,所以就需要在客户端维持一个在线用户列表,这里采用 unordered_map 结构,结构的 key 值填入用户的 ip 值,结构的 value 填入用户端的套接字。即当用户进入群聊的时候,客户端进行用户检查,该用户是否在群聊中,若不在群聊中,就将其添加到在线群聊列表中,之后客户端将该用户发送的消息在转发给群聊中的所有用户。
#pragma once#include <iostream>
#include <string>
#include <string.h>
#include <strings.h> // bzero
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>#include "Log.hpp"
Log lg;uint16_t defaultport = 8888;
std::string defaultip = "0.0.0.0";
using func_t = std::function<std::string(const std::string&)>;enum{SOCKET_ERR = 1,BIND_ERR
};class UdpServer
{
public:UdpServer(uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip), isrunning(false){}~UdpServer(){if(sockfd_ > 0)close(sockfd_);}
public:void Init(){// 1.创建udp socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd_ < 0){lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2.绑定端口号struct sockaddr_in local;bzero(&local, sizeof(local)); // 将local内部清零// 填充 socket套接字相关字段local.sin_family = AF_INET; // 当前结构体的地址类型local.sin_port = htons(port_); // 当前服务器的端口号,须将主机序列转换成网络序列local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 网络中的ip序列是4字节传输的,所以将字符串风格的ip地址转换成可以在网络中传输的4字节Ip网络序列// 开始绑定,bind本质就是将上面一系列参数设置进内核,到指定的套接字中int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));if(n < 0){lg(Fatal, "bind error, error: %d, err message: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err message: %s", errno, strerror(errno));}void CheckUser(const struct sockaddr_in& client, const std::string clientip, const uint16_t clientport){auto iter = online_user_.find(clientip);if(iter == online_user_.end()){// 找不到online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "] add to online_user..." << std::endl;}}void Broadcast(const std::string& info, const std::string clientip, const uint16_t clientport){for(const auto& user : online_user_){std::string message = "[" + clientip + ":" + std::to_string(clientport) + "]#";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (const sockaddr*)(&user.second), len);}}// 让服务器执行特定的函数功能// void Run(func_t func) void Run() {isrunning = true;char inbuffer[SIZE];while (isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);CheckUser(client, clientip, clientport); // 判断用户是否在群聊中std::string info = inbuffer;Broadcast(info, clientip, clientport); // 给每一个群聊中的用户都发送消息// // 此时服务端的数据,我们已经拿到了并将其存至了 inbuffer里面// inbuffer[n] = 0; //在结尾添加'\0'// std::string info = inbuffer; // // std::string echo_string = "server say@ " + info;// std::string echo_string = func(info);// // 向客户端发送信息// ssize_t r = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);// if(r < 0)// {// lg(Warning, "sendto error, errno: %d, err message: %s", errno, strerror(errno));// continue;// }}}
private:int sockfd_; // 网络文件描述符std::string ip_; // IPuint16_t port_; // 表明服务器进程的端口号bool isrunning; // 服务器是否运行std::unordered_map<std::string, struct sockaddr_in> online_user_;
};
// Main.cc
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include "UdpServer.hpp"void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << ", port[1024+]" << std::endl;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> usvr(new UdpServer(port));usvr->Init();usvr->Run();return 0;
}
4.2、Client 客户端
群聊中的客户端共有两个功能,一是用户发送数据,二是获取群聊中其他用户发送的信息,即本质就是获取服务端中的信息,因为其他用户发送信息数据是往服务器上发送的。所以就需要两个线程来执行两个不同的功能。一线程来发送数据给服务器,二线程是获取服务器上其他用户发送的信息。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>using namespace std;
Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << ", serverip serverport" << std::endl;
}struct ThreadData
{struct sockaddr_in server;int sockfd;std::string serverip;
};// 接收服务端其他用户发送的消息
void* recv_message(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);char buffer[1024];while (true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cerr << buffer << std::endl;}}}// 给服务端发送消息
void* sender_message(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);std::string message;socklen_t len = sizeof(td->server);while(true){std::cout << "Please Enter@ ";getline(cin, message);// 给谁发sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)(&td->server), len);}
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}// 获取 serverip 和 serverportstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 建立套接字,向服务器当中发信息struct ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport); td.server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(td.server);td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(td.sockfd < 0){std::cout << "socket error" << std::endl;return 1;}td.serverip = serverip;pthread_t recvr, sender; // 两个线程pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, sender_message, &td);pthread_join(recvr, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}
五:结语
今天的分享到这里就结束了,今日给大家分享了关于进程的终止情况。如果各位看官觉得还不错的话,可以三连支持一下欧。各位的支持就是捣蛋鬼前进的最大动力!