【Linux修行路】网络套接字编程——UDP
目录
⛳️推荐
前言
六、Udp Server 端代码
6.1 socket——创建套接字
6.2 bind——将套接字与一个 IP 和端口号进行绑定
6.3 recvfrom——从服务器的套接字里读取数据
6.4 sendto——向指定套接字中发送数据
6.5 绑定 ip 和端口号时的注意事项
6.5.1 云服务器禁止直接绑定公网 ip
6.5.2 绑定本地环回地址
6.5.2 端口号也不能胡乱绑定
6.6 服务端完整代码
七、Udp Client 端代码
八、基于 Udp 的指令处理
九、基于 Udp 的聊天室
9.1 server 端
9.1.1 地址转换函数
9.2 client 端
⛳️推荐
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站
前言
本篇文章接上一篇 【Linux修行路】初识网络套接字编程,所以目录号从六开始。
六、Udp Server 端代码
6.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_UNIX
决定了要用一个绝对路径名作为地址,AF_INET6
(IPv6)。 -
type:指定了 socket 的类型,如
SOCK_STREAM
(流式套接字)、SOCK_DGRAM
(数据报式套接字)等等。 -
protocol:指定协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)、IPPROTO_SCTP(STCP 传输协议)、IPPROTO_TIPC(TIPC 传输协议)。
-
返回值:一个文件描述符,创建套接字的本质其实就是打开一个文件。
void Init()
{// 1. 创建udp socketsockfd_ = 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 is created successful, sockfd: %d", sockfd_);
}
6.2 bind——将套接字与一个 IP 和端口号进行绑定
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
void Init()
{// 1. 创建 udp socketsockfd_ = 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 is created successful, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 将内容清空成0local.sin_family = AF_INET; // 表明当前结构体的类型local.sin_port = htons(port_); // 当前服务器的端口号local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将字符串风格的 ip 地址转化成 4 字节的网络序列// bind 的本质就是把上面这个参数设置进内核,设置到指定的套接字当中int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));if(n < 0) {lg(Fatal, "bind error, error: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success!");
}
因为是网络通信,所以首先定义一个 struct sockaddr_in
类型的对象,该对象中有四个字段,分别是:sin_family
(表明当前结构体的类型)、sin_port
(端口号)、sin_addr
(ip地址)、sin_zero
(填充字段)。我们需要自己设置前三个字段的信息。其中 sin_family
的值和 socket
函数中的 domain
参数保持一致;其次,端口号和 ip 地址,是需要再网络中传输的,因此要把主机序列转换成网络序列。其中使用 htons
将端口号从主机转成网络序列。ip 地址实际上是一个 4 字节的 uint32_t
类型,但是为了配合用户的使用习惯,我们让用户输入的 ip 地址是一个 string
类型,例如 “xxx.xxx.xxx.xxx” 的点分形式,因此 ip 地址的转换有两步,分别是将string
类型转换成 uint32_t
类型,然后再从主机转化成网络序列。这两个转化使用 inet_addr
接口就可以实现,其次 sin_addr
的类型是 struct in_addr
,该结构体中就只有一个字段 in_addr_t s_addr;
其中 in_addr_t
就是 uint32_t
。
bind
本质就是将我们上一步中创建的套接字与一个 ip 和端口号建立关联。
6.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);
-
buf:接收缓冲区。
-
len:接收缓冲区的大小。
-
flags:默认设置为 0,表示阻塞。
-
src_addr:输出型参数,获取客户端的套接字信息,也就是获取客户端的 ip 和端口号信息。因为是
udp
网络通信,所以这里传入的还是struct sockaddr_in
类型的对象地址。 -
addrlen:这里就是
struct sockaddr_in
对象的大小。 -
返回值:成功会返回获取到数据的字节数;失败返回 -1。
void Run()
{isrunning_ = true;while (isrunning_){char buffer[size];struct sockaddr_in client;socklen_t len = sizeof(client);// 从当前服务器的套接字中读取数据int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}buffer[ret] = 0;std::string info = buffer;std::string echo_string = "server echo# <b>" + info;}
}
小Tips:我们发送和接收的数据内容是不需要我们自己进行主机专网络,再从网络转主机的,数据内容会由 recvfrom
函数和 sendto
函数自动帮我们进行转换。
6.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。
-
dest_addr:接收方的套接字信息,这里也就是客户端的套接字信息。
-
addrlen:
struct sockaddr_in
对象的大小。
void Run()
{isrunning_ = true;while (isrunning_){// 从套接字中读取数据char buffer[num];struct sockaddr_in client;socklen_t len = sizeof(client);int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}buffer[ret] = 0;std::string info = buffer;std::string echo_string = "server echo# <b>" + info;// 向 client端 发送数据int n = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);if(n < 0){lg(Warning, "sendto error, error: %d, err string: %s", errno, strerror(errno));continue; }}
}
到这里,服务端的代码就编写的差不多了,接下来可以使用 netstat -naup
指令看看效果,其中 net
就表示网络,stat
就表示状态,n
选项是把所有能显示成数字的信息都以数字的形式进行显示;a
表示所有;u
表示 udp;p
表示显示 PID 信息。
6.5 绑定 ip 和端口号时的注意事项
6.5.1 云服务器禁止直接绑定公网 ip
一般建议服务器端的代码 bind 时绑定 ip 0.0.0.0,表示任意地址绑定,这样只要是发送到这台主机的数据,该服务器进程都能收到,然后根据端口号向上交付。因为一个服务器可能有多个 ip,如果服务端的进程只 bind 某一个固定的 ip,那么通过其它 ip 发送到该服务器的数据,这个进程就无法收到。local.sin_addr.s_addr = INADDR_ANY
。
6.5.2 绑定本地环回地址
任何服务器进程都可以绑定 127.0.0.1
这个 ip 地址,这个 ip 地址叫做本地环回地址,绑定了这个地址后,该进程不会向网络中发送数据,但是还是会走网络协议栈,通常用来进行 CS 的测试。
6.5.2 端口号也不能胡乱绑定
#include "UdpServer.hpp"
#include <memory>int main()
{std::unique_ptr<UdpServer> svr(new UdpServer(1023));svr->Init();svr->Run();return 0;
}
一般 [0, 1023] 是系统内定的端口号,都要有固定的应用层协议使用,例如:http
(80)、https
(443),端口号的范围是 0~65535,建议我们平时在自己的代码中就往大了去绑。
6.6 服务端完整代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "log.hpp"
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <functional>#define num 1024extern Log lg;enum
{SOCKET_ERR = 1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";using func_t = std::function<std::string(const std::string &)>;class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): ip_(ip), port_(port), isrunning_(false){}void Init(){// 1. 创建 udp socketsockfd_ = 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 is created successful, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 将内容清空成0local.sin_family = AF_INET; // 表明当前结构体的类型local.sin_port = htons(port_); // 当前服务器的端口号local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将字符串风格的 ip 地址转化成 4 字节的网络序列// bind 的本质就是把上面这个参数设置进内核,设置到指定的套接字当中int n = bind(sockfd_, (struct sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, error: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success!");}void Run(func_t func) // 参数是数据处理函数{isrunning_ = true;while (isrunning_){// 从套接字中读取数据char buffer[num];struct sockaddr_in client;socklen_t len = sizeof(client);int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}printf("client say@ %s\n", buffer);buffer[ret] = 0;std::string info = buffer;std::string echo_string = func(info); // 数据处理// 向 client端 发送数据int n = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);if(n < 0){lg(Warning, "sendto error, error: %d, err string: %s", errno, strerror(errno));continue; }}}~UdpServer(){close(sockfd_);}private:int sockfd_; // 网络文件描述符std::string ip_; // 主机的 ip 地址uint16_t port_; // 服务器的端口号bool isrunning_; // 是否在运行
};
#include "UdpServer.hpp"
#include <memory>
#include <string>void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " port[1024+]" << std::endl;
}std::string Hander(const std::string &str)
{std::string res = "Server get a message: " + str;return res;
}int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]); std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(Hander);return 0;
}
七、Udp Client 端代码
因为一个端口号只能被一个进程 bind,客户端的应用是非常多的,如果在客户端采用静态 bind,那可能会出现两个应用同时 bind 同一个端口号,此时就注定了这两个应用一定是不能同时运行的。为了解决这个问题,一般不建议客户端 bind 一个固定的端口,而是由操作系统来进行动态的 bind,这样就可以避免端口号发生冲突。这也间接说明,对一个 client 端的进程来说,它的端口号是几并不重要,只要能够标识该进程在主机上的唯一性就可以。因为,一般都是由 clinet 端主动的向 server 端发送消息,所以 client 一定是能够知道 client 端的端口号。相反,服务器的端口号必须是确定的。因此,在编写客户端的代码时,第一步就是创建套接字,创建完无需进行 bind,直接向服务器发送数据,发送的时候,操作系统会为我们进行动态 bind。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <memory>
#include <string>
#include <cstring>
#include <arpa/inet.h>using namespace std;void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " serverip serverport" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);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);// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;exit(1);}// 2. 接下来直接发送数据string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);// 发送数据sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);struct sockaddr_in temp;socklen_t temp_len = sizeof(temp);// 接收服务器数据ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &temp_len);if(s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
八、基于 Udp 的指令处理
服务端在收到用户端的数据后,会对数据进行加工处理,然后再返回给客户端,可以将数据处理的过程独立出来,将数据处理的函数作为参数传递给服务端的 Run
方法,当前这个场景就是基于 Udp 的,客户端输入指令,客户端执行对应的指令,并将执行结果返回给用户。这里服务端指令的执行通过调用 popen
函数即可。
#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
popen
函数会调用 fork
创建子进程,然后子进程执行程序替换,去执行指令。其中 command
参数就是要执行的指令,type
使用 "r"
表示读取,使用 "w"
表示写入,根据这个参数,popen
会建立管道连接到子进程的标准输入输出设备或标准输入设备,然后返回一个文件指针,随后进程便可利用此文件指针来读取子进程的输出设备或者写入到子进程的标准输入设备中。此外,所有使用文件指针 (FILE*
) 操作的函数也都可以使用,出了 fclose
以外。返回值:如果床工则返回文件指针,否则返回 NULL
,错误原因存于 errno
中。
客户端的主题代码不动,只用将 ExcuteCommand
函数作为 svr->Run();
的参数传递进去即可。
bool SafeCheak(const std::string &cmd)
{std::vector<std::string> key_word = {"rm", "mv", "cp", "kill", "sudo", "unlink", "uninstall", "yum", "top"};for(auto & key : key_word){if(cmd.find(key) != std::string::npos){return false;}}return true;
} std::string ExcuteCommand(const std::string &cmd)
{std::cout << "get a message, cmd: %s" << std::endl;// 指令检查if(!SafeCheak(cmd)) return "This action is prohibited";FILE *fp = popen(cmd.c_str(), "r");if(fp == NULL){perror("popen");return "error";}// 读取执行结果std::string result;char buffer[4096];while(true){char *ret = fgets(buffer, sizeof(buffer), fp);if(ret == NULL) break;result += buffer;}fclose(fp);return result;
}
九、基于 Udp 的聊天室
9.1 server 端
因为是聊天室,只要是进入该聊天室的用户,在该聊天室中发送的消息,应该是可以被所有在线用户看到的,所以我们需要在客户端维护一张在线用户列表,采用 unordered_map
结构,让用户的 ip
作为 key
值,用户端的套接字作为 value
,当一个客户进入该聊天室的时候,客户端应该进行检查,看其是否在在线列表中,如果不在,应该先将其加入到在线列表。然后客户端需要将该用户发送的信息,再转发给其他所有在线用户,因此需要去遍历在线用户列表进行 sendto
。服务端在转发用户消息前,先进行轻加工,在消息前加上用户端的 ip 和端口,用来标识该信息是谁发送的。因此服务端在接收到用户端信息后,需要对从网络中收到的用户套接字信息进行网路转主机操作。对于端口号,可以采用 ntohs
接口将网络序列转换成主句序列,对于 ip,可以采用 inet_ntoa
将 4 字节的网络 ip 序列转化成字符串类型的主机序列。
// UdpServer.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "log.hpp"
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <functional>
#include <unordered_map>#define num 1024extern Log lg;enum
{SOCKET_ERR = 1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";using func_t = std::function<std::string(const std::string &, const std::string &, const uint16_t &)>;class UdpServer
{
private:void CheckUser(const sockaddr_in &client, const std::string &client_ip, uint16_t client_port){auto pos = userinfo_.find(client_ip);if (pos == userinfo_.end()){userinfo_.insert({client_ip, client});printf("[%s-%d] Enter the chat room\n", client_ip.c_str(), client_port);}}void Broadcast(const std::string &info, const std::string &client_ip, uint16_t client_port){for(auto &user : userinfo_){std::string message = "[";message += client_ip;message += "-";message += std::to_string(client_port);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr *)&(user.second), sizeof(user.second));}}public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): ip_(ip), port_(port), isrunning_(false){}void Init(){// 1. 创建 udp socketsockfd_ = 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 is created successful, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 将内容清空成0local.sin_family = AF_INET; // 表明当前结构体的类型local.sin_port = htons(port_); // 当前服务器的端口号local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将字符串风格的 ip 地址转化成 4 字节的网络序列// bind 的本质就是把上面这个参数设置进内核,设置到指定的套接字当中int n = bind(sockfd_, (struct sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, error: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success!");}// 数据传输——(获取和发送)void Run(){isrunning_ = true;while (isrunning_){// 从套接字中读取数据char buffer[num];struct sockaddr_in client;socklen_t len = sizeof(client);int ret = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);if (ret < 0){lg(Warning, "recvfrom error, error: %d, err string: %s", errno, strerror(errno));continue;}buffer[ret] = 0;uint16_t client_port = ntohs(client.sin_port); // 获取client 端的端口号std::string client_ip = inet_ntoa(client.sin_addr); // 获取 client 端的 ip 地址CheckUser(client, client_ip, client_port);std::string info = buffer;Broadcast(info, client_ip, client_port); // 将收到的消息广播给所有的在线用户// std::string info = buffer;// std::string echo_string; // = func(info, client_ip, client_port);// // 向 client端 发送数据// int n = sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len);// if (n < 0)// {// lg(Warning, "sendto error, error: %d, err string: %s", errno, strerror(errno));// continue;// }}}~UdpServer(){close(sockfd_);}private:int sockfd_; // 网络文件描述符std::string ip_; // 主机的 ip 地址uint16_t port_; // 服务器的端口号bool isrunning_; // 是否在运行std::unordered_map<std::string, sockaddr_in> userinfo_; // 维护一张用户信息表。
};
// main.cc
#include "UdpServer.hpp"
#include <memory>
#include <string>
#include <vector>
#include <iostream>void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " port[1024+]" << std::endl;
}int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]); std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run();return 0;
}
补充:upd
的 socket
是全双工的,可以同时读数据和发数据。
9.1.1 地址转换函数
将字符串转化成 4 字节网络序列:
#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);
int inet_pton(int af, const char *src, void *dst);
网络序列转化成字符串:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_ntoa
函数的返回值是一个地址,那转化出来的字符串在哪呢?答案是,在 inet_ntoa
函数的内部为我们申请了一块内存来保存 ip
字符串的结果,main
手册上说,inet_ntoa
函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动释放。但这也导致在一个进程中第二次调用 inet_ntoa
函数时,会对上一次的转换结果进行覆盖。在 APUE
中明确的提出 inet_ntoa
不是线程安全的函数,所以在多线程编程下建议使用 inet_ntop
函数,这个函数由调用者自己提供一个缓冲区保存结果,可以规避线程安全问题。
9.2 client 端
client
端主要有两个功能,一是该用户发送数据,二是该用户获取其他用户发送的消息,本质是获取服务端的消息,因为其他用户的消息是先交给服务端的。因此这里需要两个线程,一个线程用来获取用户输入,将用户的输入消息发送给服务器,另一个线程用来接收服务器的消息,采用两个线程的本质原因是,在获取用户输入的时候,如果用户一直没有进行输入,那么会阻塞住,就收不到服务器的消息,所以这里采用两个线程将获取用户输入和获取服务端的消息分开。这两个线程一个线程往套接字里面进行写入,一个从套接字里面进行读取,看似在访问同一份资源,但实际上,Udp 套接字是一个全双工的,发数据和收数据都有自己独立的缓冲区,因此不存在线程安全。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <memory>
#include <string>
#include <cstring>
#include <arpa/inet.h>using namespace std;struct ThreadData
{int socckfd;sockaddr_in server;
};void Usage(const char *command)
{std::cout << "\n\tUsage: " << command << " serverip serverport" << std::endl;
}void *send_rountine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;while(true){cout << "Please Enter@ ";getline(cin, message);// 发送数据sendto(td->socckfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), sizeof(td->server));}
}void *recv_rountine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while(true){struct sockaddr_in temp;socklen_t temp_len = sizeof(temp);// 接收服务器数据ssize_t s = recvfrom(td->socckfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &temp_len);if(s > 0){buffer[s] = 0;cerr << buffer << endl;}}
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;exit(1);}// 创建俩线程,一个获取用户输入向 server 发送,一个接收 server 端消息pthread_t send, recv;ThreadData *threaddata = new ThreadData();threaddata->socckfd = sockfd;threaddata->server.sin_family = AF_INET;threaddata->server.sin_port = htons(serverport);threaddata->server.sin_addr.s_addr = inet_addr(serverip.c_str()); pthread_create(&send, nullptr, send_rountine, threaddata);pthread_create(&recv, nullptr, recv_rountine, threaddata);pthread_join(send, nullptr);pthread_join(recv, nullptr);close(sockfd);return 0;
}
这里有个小细节,就是将用户输入消息的终端和显示消息的终端分离,在打印消息时,采用 cerr
,然后在启动 client
的时候,将标准错误重定向到另一个终端。
🎁结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是我前进的动力!
相关文章:

【Linux修行路】网络套接字编程——UDP
目录 ⛳️推荐 前言 六、Udp Server 端代码 6.1 socket——创建套接字 6.2 bind——将套接字与一个 IP 和端口号进行绑定 6.3 recvfrom——从服务器的套接字里读取数据 6.4 sendto——向指定套接字中发送数据 6.5 绑定 ip 和端口号时的注意事项 6.5.1 云服务器禁止直接…...

哈希表数据结构学习
哈希表数据结构学习 哈希表基本概念哈希方法单值哈希与多值哈希哈希冲突1. 开放寻址法(Open Addressing)2. 链地址法(Chaining)3. 再哈希法(Rehashing)4. 建立公共溢出区(Overflow Area…...

数据结构——“二叉搜索树”
二叉搜索树是一个很重要的数据结构,它的特殊结构可以在很短的时间复杂度找到我们想要的数据。最坏情况下的时间复杂度是O(n),最好是O(logn)。接下来看一看它的接口函数的实现。 为了使用方便,这里采用模版的方式: 一、节点 temp…...
Java零基础-Java对象详解
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互…...
从Prompt到创造:解锁AI的无限潜能
文章目录 🍊AI内容创作核心:提示词Prompt1 什么是提示词工程?1.1 提示词的原理是什么?1.2 提示词工程师:百万年薪的职业?1.3 谁都能成为提示词工程师吗? 2 提示词书写的基本技巧3 常见的提示词框架3.1 CO-…...

sqlgun靶场攻略
打开界面 1.输入框测试回显点 -1union select 1,2,3#出现回显点 2.查看数据库名 -1union select 1,2,database()# 3.查看表名 -1union select 1,2,group_concat(table_name) from information_schema.tables where table_schemasqlgunnews# 4.查看admin表中列名 -1union se…...

《网络协议 - HTTP传输协议及状态码解析》
文章目录 一、HTTP协议结构图二、HTTP状态码解读1xx: 信息响应类2xx: 成功响应类3xx: 重定向类4xx: 客户端错误类5xx: 服务器错误类 一、HTTP协议结构图 二、HTTP状态码解读 HTTP状态码(英语:HTTP Status Code)是用以表示网页服务器超文本传…...

9.11 QT ( Day 4)
一、作业 1.Widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> //定时器类 #include <QTime> #include <QtTextToSpeech> //文本转语音类QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEcl…...

利用AI驱动智能BI数据可视化-深度评测Amazon Quicksight(四)
简介 随着生成式人工智能的兴起,传统的 BI 报表功能已经无法满足用户对于自动化和智能化的需求,今天我们将介绍亚马逊云科技平台上的AI驱动数据可视化神器 – Quicksight,利用生成式AI的能力来加速业务决策,从而提高业务生产力。…...

2024.9最新:CUDA安装,pytorch库安装
目录 一、CUDA安装 1.查看自己电脑适配的CUDA的最高版本 2.安装CUDA 3.检查环境变量是否配置,安装是否成功 二、pytorch库安装 1.pytorch库下载 2.选择合适的版本 3.查看版本 一、CUDA安装 1.查看自己电脑适配的CUDA的最高版本 在命令提示符里输入nvidia-…...

Vue3.0组合式API:setup()函数
1、什么是组合式API Vue 3.0 中新增了组合式 API 的功能,它是一组附加的、基于函数的 API,可以更加灵活地组织组件代码。通过组合式 API 可以使用函数而不是声明选项的方式来编写 Vue 组件。因此,使用组合式 API 可以将组件代码编写为多个函…...

利用AI驱动智能BI数据可视化-深度评测Amazon Quicksight(三)
简介 随着生成式人工智能的兴起,传统的 BI 报表功能已经无法满足用户对于自动化和智能化的需求,今天我们将介绍亚马逊云科技平台上的AI驱动数据可视化神器 – Quicksight,利用生成式AI的能力来加速业务决策,从而提高业务生产力。…...

2022高教社杯全国大学生数学建模竞赛C题 问题一(1) Python代码演示
目录 问题 11.1 对这些玻璃文物的表面风化与其玻璃类型、纹饰和颜色的关系进行分析数据探索 -- 单个分类变量的绘图树形图条形图扇形图雷达图Cramer’s V 相关分析统计检验列联表分析卡方检验Fisher检验绘图堆积条形图分组条形图分类模型Logistic回归随机森林import matplotlib…...

Qt QSerialPort数据发送和接收DataComm
文章目录 Qt QSerialPort数据发送和接收DataComm2.添加 Qt Serial Port 模块3.实例源码 Qt QSerialPort数据发送和接收DataComm Qt 框架的Qt Serial Port 模块提供了访问串口的基本功能,包括串口通信参数配置和数据读写,使用 Qt Serial Port 模块就可以…...

macOS上谷歌浏览器的十大隐藏功能
谷歌浏览器(Google Chrome)在macOS上拥有一系列强大而隐蔽的特性,这些功能能显著提高您的浏览体验。从多设备同步到提升安全性和效率,这些被低估的功能等待着被发掘。我们将逐步探索这些功能,帮助您最大化利用谷歌浏览…...

【C++篇】C++类与对象深度解析(二):类的默认成员函数详解
文章目录 【C篇】C类与对象深度解析(二)前言1. 类的默认成员函数2. 构造函数2.1 函数名与类名相同2.2 无返回值2.3 对象实例化时系统会自动调用2.4 构造函数可以重载2.5 默认构造函数的生成规则2.6 无参构造函数与全缺省构造函数的关系2.7 内置类型与自定…...
Linux2-mkdir,touch,cat,more
1.相对路径和绝对路径 cd用于切换目录,对于路径可以用相对路径和绝对路径 例如:cd /home/user/public和cd public效果一样,都是将目录切换到HOME文件夹下的public文件夹 2.特殊路径符 .表示当前目录 ..表示上级目录 ~表示HOME目录 3.m…...

AI 时代程序员的应变之道
一、AI 浪潮来袭,编程界风云变幻 随着 AIGC 大语言模型如 ChatGPT、Midjourney、Claude 等的涌现,AI 辅助编程工具日益普及,程序员的工作方式正经历着深刻的变革。 分析公司 OReilly 日前发布的《2023 Generative AI in the Enterprise》报告…...
SQL编程题复习(24/9/16)
练习题 x40 10-74 获取商品表中商品名称含有“pad”的商品10-75 获取指定商品的商品分类名称(多表查询)10-76 为sh_goods表添加一行记录10-77 检索出sh_goods表中每项keyword对应的商品数量,查询结果显示字段依据输出样例设置10-78 查询sh_go…...
运维工程师面试整理-操作系统
在运维工程师的面试中,操作系统相关的知识通常是重中之重,尤其是Linux/Unix系统。以下是针对操作系统部分的一些详细内容,帮助你更好地准备面试。 1. Linux/Unix 基础 ● 常用命令 ○ 文件和目录管理:ls, cd, cp, mv, rm, mkdir, rmdir, find, grep, awk, sed...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...

idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...