套接字编程 --- 二

news/2024/5/25 9:05:18/文章来源:https://blog.csdn.net/m0_62229058/article/details/136568492

目录

1. 相关接口说明

1.1. popen 接口

1.2. strcasestr 接口

2. UDP --- demo2

2.1. Udp_Client.cc

2.2. Udp_Server.cc

2.3. Udp_Server.hpp

2.4. demo2 总结 

3. UDP --- demo3

3.1. Thread.hpp

3.2. Udp_Client.cc

3.2. Udp_Server.cc

3.3. Udp_Server.hpp

3.4. demo3总结 


接上篇套接字编程 --- 一,继续。

1. 相关接口说明

1.1. popen 接口

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE* stream)

popen 函数: 在函数内部调用 fork() 和 pipe() ,并创建标准的输出或输入管道。

popen 内部会执行command命令,并将结果写入管道。

我们可以通过返回值 FILE*, 读取管道内容 (执行命令的结果)。

  1. command:需要执行的 shell 命令或进程名。
  2. mode:打开管道的模式,可以是 " r "(读模式)或 " w "(写模式)。
  3. 若模式为 " r ",则返回可用于读取管道输出流的 FILE 指针。
  4. 若模式为 " w ",则返回可用于向管道输入流写入数据的 FILE 指针。

在使用完毕后,必须使用 pclose() 函数来关闭由 popen() 函数打开的管道,并回收相关资源 (等等子进程退出,回收子进程资源),以避免出现资源泄露的情况。

pclose() 函数将阻塞调用进程,直到被调用进程终止并关闭它所打开的管道。

popen() 和 pclose() 函数的结合机制类似于 Linux 系统上的 shell 命令中管道(|)的功能,即将一个进程的输出连接到另一个进程的输入,可以方便地实现进程间的通信。

1.2. strcasestr 接口

#include <string.h>
char *strcasestr(const char *haystack, const char *needle);

strcasestr 函数用于在一个字符串中查找另一个字符串,并返回第一次出现的匹配子串的指针,不区分大小写。

其参数如下:

  1. haystack:要搜索的字符串,即被查找的字符串。
  2. needle:要查找的子字符串,即需要匹配的字符串。

函数将会在 haystack 字符串中查找第一个不区分大小写的 needle 子字符串,并返回该子字符串在 haystack 中的位置。如果未找到匹配的子串,则返回 nullptr。

2. UDP --- demo2

如果服务端收到客户端发送的信息是一个字符串,如果这个字符串是一串命令呢 ?

因此此时服务端的目的:
将客户端传递过来的命令,进行分析处理执行,服务端执行完毕,并将执行结果返回给客户端。

像Date.hpp、Log.hpp、Makefile和套接字编程 --- 一 里面的demo1一致,在这里就不重复了。

2.1. Udp_Client.cc

#include "Date.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <cstring>// 下面的这四个头文件,我们称之为网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <unistd.h>#define CLIENT_BUFFER 1024void Usage(void)
{printf("please usage: ./Client ServerIp  ServerPort\n");
}int main(int arg, char* argv[])
{if(arg != 3){Usage();exit(-2);}// 客户端创建套接字// 这里的PF_INET 是 AF_INET的封装int client_sock = socket(PF_INET, SOCK_DGRAM, 0);if(client_sock == -1){LogMessage(FATAL, "%s\n", "client create sock failed");exit(1);}// 这里有一个问题, 客户端需不需要bind呢?// 答案: 肯定是需要的, 但是一般 client 不会显示的bind。换言之,程序员一般不会在客户端 bind。// client 是一个客户端, 是普通用户下载安装启动使用的, 如果程序员自己bind了,// 那么是不是就要求客户端一定bind了一个固定的ip和port,// 那么万一其他的客户端提前占用了这个port呢?那不就会导致bind失败吗?// 因为一个端口号只能绑定一个进程。// 因此,客户端一般不需要显式的bind指定port,而是让OS自动随机选择bind;// 可是操作系统是什么时候做的呢?// 1. 客户端向服务端发送数据// 因为客户端是向服务器发送数据,因此需要服务器的地址信息 IP + port;// 即需要服务器的端口和IP,通过命令行参数 (注意是 服务器的IP和port哦)。// 注意, 我们这里都是主机数据// 因此要转化为网络字节序sockaddr_in server;memset(&server,0, sizeof(server));// 填充sin_familyserver.sin_family = AF_INET;// 填充sin_addr(服务器的IP)server.sin_addr.s_addr = inet_addr(argv[1]);// 填充sin_port(服务器的端口)server.sin_port = htons(atoi(argv[2]));socklen_t server_len = sizeof(server);char buffer[CLIENT_BUFFER] = {0};while(true){std::string client_message;std::cout << "client: " << "请输入信息" << std::endl;std::getline(std::cin, client_message);// 如果客户端输入 "quit" , 退出客户端if(client_message == "quit")break;// 当client 首次发送消息给服务器的时候,// OS会自动给客户端bind 它的套接字以及IP和port (即绑定客户端的 ip + port);// 即第一次sendto的时候,操作系统会自动绑定ssize_t real_client_write = sendto(client_sock, client_message.c_str(), client_message.size(), 0, \reinterpret_cast<const struct sockaddr*>(&server), server_len);if(real_client_write < 0){LogMessage(ERROR, "client write size < 0\n");exit(2);}// 2. 读取返回数据 (服务端发送给客户端的数据)buffer[0] = 0;// 此时客户端发送的就是命令, 服务端处理后, 将处理数据返回给客户端// 因为 sockaddr_in 是一个输出型参数, 因此调用完后,其实它就是发送方的地址信息// 以及发送方的这个结构体(缓冲区)的长度 (输入输出型参数)sockaddr_in server;bzero(&server, sizeof server);socklen_t server_addr_len = 0;ssize_t real_client_read = recvfrom(client_sock, buffer, CLIENT_BUFFER - 1, 0, \reinterpret_cast<struct sockaddr*>(&server), &server_addr_len);if(real_client_read > 0){// 当返回值 > 0, 代表着读取成功// 客户端原封不动的打印一下这个信息buffer[real_client_read] = 0;printf("server: %s\n", buffer);}}if(client_sock >= 0)close(client_sock);return 0;
}

2.2. Udp_Server.cc

#include "Udp_Server.hpp"void standard_usage(void)
{printf("please usage: ./Server port\n");
}int main(int argc, char* argv[])
{if(argc != 2){standard_usage();exit(1);}// 传递端口号即可Xq::udp_server* server = new Xq::udp_server(atoi(argv[1]));server->init_server();server->start();delete server;return 0;
}

2.3. Udp_Server.hpp

#ifndef __UDP_SERVER_HPP_
#define __UDP_SERVER_HPP_#include "Date.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <cstring>// 下面的这四个头文件,我们称之为网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <unistd.h>// 服务端缓冲区大小
#define SER_BUFFER_SIZE 1024namespace Xq
{class udp_server{public:// 需要显示传递服务器的 portudp_server(uint16_t port, const std::string ip = ""):_ip(ip),_port(port),_sock(-1){}void init_server(void){//1. 创建套接字 --- socket// AF_INET 是一个宏值, 在这里代表着网络套接字// SOCK_DGRAM, 标定这是数据报套接字// protocol 默认情况下都是0_sock = socket(AF_INET, SOCK_DGRAM, 0);if(_sock == -1){// 套接字创建失败对于网络通信而言是致命的LogMessage(FATAL, "%s", "socket failed");exit(1);}//2. 绑定端口号 --- bind// bind 将相应的ip和port在内核中与指定的进程强关联// 服务器跑起来就是一个进程, 因此需要通过// 服务器的IP + port 绑定服务器这个进程// 因此我们需要通过 sockaddr_in 设置地址信息struct sockaddr_in server;// 我们可以初始化一下这个对象// 通过bzero(), 对指定的一段内存空间做清0操作bzero(static_cast<void*>(&server), sizeof(server));// 初始化完毕后, 我们就需要填充字段// sockaddr_in 内部成员// in_port_t sin_port;  ---  对port的封装// struct in_addr sin_addr; --- 对ip的封装// sin_family  sa_family; --- 如果我们是网络套接字, 那么填充 AF_INET// 我们要知道, 0.0.0.0 这种IP地址我们称之为"点分十进制" 字符串风格的IP地址// 每个点分割的区域数值范围 [0, 255];// 四个区域代表着四个字节, 理论上标识一个IP地址, 其实四字节就足够了// 点分十进制的字符串风格的IP地址是给用户使用的// 在这里我们需要将其转成32位的整数 uint32_tserver.sin_family = AF_INET;// 当我们在网络通信时, 一方不仅要将自己的数据内容告诉对方// 还需要将自己的IP地址以及端口号告诉对方。// 即服务器的IP和端口号未来也是要发送给对方主机的特定进程(客户端进程)// 那么是不是我需要先将数据从 本地 发送到 网络呢?// 答案: 是的, 因此我们还需要注意不同主机内的大小端问题// 因此, 我们在这里统一使用网络字节序server.sin_port = htons(_port);// 而对于IP地址而言, 也是同理的// 只不过此时的IP地址是点分十进制的字符串// 因此我们需要先将其转为32位的整数, 在转化为网络字节序// 而 inet_addr() 这个接口就可以帮助我们做好这两件事//server.sin_addr.s_addr = inet_addr(_ip.c_str());// 作为 server 服务端来讲,我们不推荐绑定确定的IP,// 我们推荐采用任意IP的方案,即INADDR_ANY(是一个宏值), 本质就是((in_addr_t) 0x00000000)// 作为服务器, 我们可以不用暴露IP, 只暴露端口号即可。// INADDR_ANY让服务器,在工作过程中,可以从任意IP中获取数据// 如果我们在服务器端bind了一个固定IP, 那么此时这个服务器就只能// 收取某个具体IP的消息, 但如果我们采用INADDR_ANY// 那么就是告诉操作系统, 凡是给该主机的特定端口(_port)的数据都给我这个服务端// 有了这样的认识之后,服务端只需要端口,不需要传递IP了 (默认设置为 INADDR_ANY)server.sin_addr.s_addr = _ip.empty() ?  INADDR_ANY : inet_addr(_ip.c_str());// 填充 struct sockaddr_in done// 这里的 socklen_t 本质上就是 unsigned intsocklen_t server_addr_len = sizeof(server);if(bind(_sock, reinterpret_cast<const struct sockaddr*>(&server), server_addr_len) == -1){// 如果 bind 失败, 对于服务端是致命的LogMessage(FATAL, "%s\n", "bind error");exit(2);}// bind 成功// 初始化doneLogMessage(NORMAL, "%s\n", "init_server success");}void start(void){char buffer[SER_BUFFER_SIZE] = {0};for(;;){// 客户端的地址信息struct sockaddr_in client;bzero(static_cast<void*>(&client), sizeof(client));socklen_t client_addr_len = sizeof(client);buffer[0] = 0;// 1. 读取客户端数据 --- recvfrom// 当服务器收到客户端发送的数据// 那么是不是服务端还需要将后续的处理结果返回给客户端呢?// 答案: 是的. 因此除了拿到数据之外, 服务端是不是还需要客户端的地址信息(IP + port)// 因此, 我们就可以理解为什么recvfrom系统调用会要后两个参数了// struct sockaddr *src_addr 是一个输出型参数, 用来获取客户端的地址信息// socklen_t *addrlen 是一个输入型参数、 输出型参数 如何理解// 输入型: 这个缓冲区 src_addr 的初始值大小,做输入型参数// 输出型: 这个缓冲区 src_addr 的实际值, 填充sockaddr_in的实际大小,做输出型参数// flags == 0 代表阻塞式的读取数据ssize_t real_read_size = recvfrom(_sock, buffer, SER_BUFFER_SIZE - 1, 0, \reinterpret_cast<struct sockaddr*>(&client), &client_addr_len);if(real_read_size > 0 /* 代表读取成功 */){// 我们此时将这个数据当作为一个命令行字符串处理buffer[real_read_size] = 0;std::cout <<  buffer  << std::endl;// 做一层保险工作, 防止客户端调用 rm、rmdir 等命令// 检测一下 buffer 这个字符串// 我们可以通过 strcasestr 这个接口// char *strcasestr(const char *haystack, const char *needle);// 用于在一个字符串中查找另一个字符串,并返回第一次出现的匹配子串的指针// haystack: 要搜索的字符串// needle: 要查找的子字符串if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir")){// 如果出现了, 就提示一下std::string malice_argv = "坏人do: ";malice_argv += buffer;std::cout << malice_argv << std::endl;// 避免客户端被阻塞sendto(_sock, malice_argv.c_str(), malice_argv.size(), 0,\reinterpret_cast<const struct sockaddr*>(&client), client_addr_len);continue;}// 因为我们此时是将这个buffer当成命令字符串的// 调用popen// FILE *popen(const char *command, const char *type);// popen 会在内部调用 fork(), pipe()// popen内部会执行command命令, 并将结果写入管道// 我们可以通过返回值 FILE*, 读取管道内容 (执行命令的结果)// type 代表打开管道的模式, "r" ---> 读取管道  "w" ---> 写管道// 因为此时的处理结果在管道内, 因此我们已读方式打开管道, 读取数据FILE* client_result_info = popen(buffer, "r");if(client_result_info == nullptr){LogMessage(ERROR, "%s\n", "command not found");continue;}// popen调用成功// 通过FILE* 读取管道内容std::string client_message;char client_message_buffer[256] = {0};while(nullptr != fgets(client_message_buffer, 256, client_result_info)){client_message += client_message_buffer;}pclose(client_result_info);// 2. 向客户端写回数据 --- sendto// 既然我们要向客户端写回数据// 那么是不是需要, 客户端的IP、port// 我们不用过多处理, 因为 recvfrom 已经有了客户端的地址信息// 而我们就将客户端传过来的数据, 重发给客户端即可// 将服务端的处理结果返回给客户端, 即就是client_messagessize_t real_write_size = sendto(_sock, client_message.c_str(), client_message.size(), 0,\reinterpret_cast<const struct sockaddr*>(&client), client_addr_len);if(real_write_size < 0){LogMessage(ERROR, "%s\n", "write size < 0");exit(3);}}}}~udp_server(){if(_sock != -1)close(_sock);}private:// IP地址, 这里之所以用string, 想表示为点分十进制的字符串风格的IP地址std::string _ip;// 端口号uint16_t _port;// 套接字, socket系统调用的返回值,代表返回一个新的文件描述符 int _sock;};
}#endif

2.4. demo2 总结 

事实上,demo2 和  套接字编程 --- 一 中的 demo1只有在服务端处理数据不同罢了。 demo2中是将客户端的数据当成了命令行字符串处理, 借用 popen 达到命令行解析、执行命令 or 进程,并将处理结果写入管道。 服务端通过返回的文件指针读取管道内容,并将数据写回客户端。

popen 本质上是调用了 fork 和 pipe ,因此,popen处理完毕后,使用 pclose 函数来关闭文件指针并等待子进程结束。这是因为 popen 在内部创建了一个子进程,而 pclose 会等待子进程结束并返回其退出状态。

3. UDP --- demo3

如果我想完成一个群聊功能呢?

服务端收到一条消息,发送给客户端(不同的进程)。

服务端将收到的信息广播给所有客户端进程

要求客户端一直接收数据、一直发送数据。

因此我们需要将客户端改为多线程。

3.1. Thread.hpp

线程的封装,在线程池中就已经详细解释了,在这就不赘述了。

#ifndef __THREAD_HPP_
#define __THREAD_HPP_#include <iostream>
#include <pthread.h>
#include <string>
#include "Log.hpp"const int BUFFER_SIZE = 64;typedef void*(*Tfunc_t)(void*);namespace Xq
{class thread_info{public:thread_info(const std::string& name = std::string (), void* arg = nullptr):_name(name),_arg(arg){}void set_info(const std::string& name, void* arg){_name = name;_arg = arg;}std::string& get_name(){return _name;}void*& get_arg(){return _arg;}private:std::string _name;void* _arg;};class thread{public:thread(size_t num, Tfunc_t func, void* arg):_func(func),_arg(arg){// 构造线程名char buffer[BUFFER_SIZE] = {0};snprintf(buffer, BUFFER_SIZE, "%s %ld", "thread", num);_name = buffer;// 设置线程所需要的信息, 线程名 + _arg_all_info.set_info(_name, _arg);}// 创建线程void create(void){pthread_create(&_tid, nullptr, _func, static_cast<void*>(&_all_info));//std::cout << "创建线程: " << _name << " success" << std::endl;//LogMessage(NORMAL, "%s: %s %s", "创建线程", _name.c_str(), "success");}pthread_t get_tid(){return _tid;}private:std::string _name;  // 线程名Tfunc_t _func;     // 线程的回调pthread_t _tid;   //线程IDthread_info _all_info;  // 装载的是 线程名 + _arg;// 线程参数, 未来我们会将其和线程名封装到一起(thread_info),整体传递给线程void* _arg; };
}#endif

3.2. Udp_Client.cc

因为我们要求,客户端收发是同时的, 因此需要多线程,即一个线程进行发数据,一个线程进行写数据。

#include "Date.hpp"
#include "Log.hpp"
#include "Thread.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <pthread.h>// 下面的这四个头文件,我们称之为网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <unistd.h>#define CLIENT_BUFFER 1024// 将套接字设置为全局, 方便新线程访问
int client_sock = -1; void Usage(void)
{printf("please usage: ./Client ServerIp  ServerPort\n");
}void* send_data(void* arg)
{Xq::thread_info *T_info = static_cast<Xq::thread_info*>(arg);struct sockaddr_in* addr = static_cast<struct sockaddr_in*>(T_info->get_arg());int addrlen = sizeof (*addr);while(true){std::string client_message;std::cerr << "client: " << "请输入信息" << std::endl;std::getline(std::cin, client_message);// 如果客户端输入 "quit" , 退出客户端if(client_message == "quit")exit(0);// 当client 首次发送消息给服务器的时候,// OS会自动给客户端bind 它的套接字以及IP和port (即绑 ip + port);// 即第一次sendto的时候,操作系统会自动绑定ssize_t real_client_write = sendto(client_sock, client_message.c_str(),client_message.size(), 0, reinterpret_cast<const struct sockaddr*>(addr), \addrlen);if(real_client_write < 0){LogMessage(ERROR, "client write size < 0\n");exit(2);}}return nullptr;
}void* recv_data(void* arg)
{arg = nullptr;char buffer[CLIENT_BUFFER] = {0};while(true){buffer[0] = 0;// 因为 sockaddr_in 是一个输出型参数, 因此调用完后,其实他就是发送方的地址信息// 以及发送方的这个结构体(缓冲区)的长度 (输入输出型参数)sockaddr_in server;bzero(&server, sizeof server);socklen_t server_addr_len = sizeof server;ssize_t real_client_read = recvfrom(client_sock, buffer, CLIENT_BUFFER - 1, 0, \reinterpret_cast<struct sockaddr*>(&server), &server_addr_len);if(real_client_read > 0){// 当返回值 > 0, 代表着读取成功// 客户端原封不动的打印一下这个信息buffer[real_client_read] = 0;std::cout << buffer;fflush(stdout);}}return nullptr;
}int main(int arg, char* argv[])
{if(arg != 3){Usage();exit(-2);}// 客户端创建套接字// 我们预期是客户端进程有两个新线程// 一个新线程用来向服务端发送数据 --- send_thread// 一个新线程用来向服务端读取数据 --- recv_thread// 而这里的套接字, 两个线程都要用// 但是这两个线程不会修改这个套接字// 即不涉及线程安全问题, 因此我们将其改为全局的// 这里的PF_INET 是 AF_INET的封装client_sock = socket(PF_INET, SOCK_DGRAM, 0);if(client_sock == -1){LogMessage(FATAL, "%s\n", "client create sock failed");exit(1);}sockaddr_in server;memset(&server,0, sizeof(server));// 填充sin_familyserver.sin_family = AF_INET;// 填充sin_addr(服务器的IP)server.sin_addr.s_addr = inet_addr(argv[1]);// 填充sin_port(服务器的端口)server.sin_port = htons(atoi(argv[2]));Xq::thread* send_thread = new Xq::thread(1, send_data, static_cast<void*>(&server));send_thread->create();Xq::thread* recv_thread = new Xq::thread(2, recv_data, nullptr);recv_thread->create();pthread_join(send_thread->get_tid(), nullptr);pthread_join(recv_thread->get_tid(), nullptr);delete send_thread;delete recv_thread;if(client_sock >= 0)close(client_sock);return 0;
}

3.2. Udp_Server.cc

#include "Udp_Server.hpp"void standard_usage(void)
{printf("please usage: ./Server port\n");
}int main(int argc, char* argv[])
{// 服务端我们不用显式传递IP了, 默认用INADDR_ANY// 因此, 我们只需要两个命令行参数if(argc != 2){standard_usage();exit(1);}// 传递端口号即可Xq::udp_server* server = new Xq::udp_server(atoi(argv[1]));server->init_server();server->start();delete server;return 0;
}

3.3. Udp_Server.hpp

#ifndef __UDP_SERVER_HPP_
#define __UDP_SERVER_HPP_#include "Date.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <map>// 下面的这四个头文件,我们称之为网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <unistd.h>// 服务端缓冲区大小
#define SER_BUFFER_SIZE 1024namespace Xq
{class udp_server{public:// 需要显示传递服务器的 portudp_server(uint16_t port, const std::string ip = ""):_ip(ip),_port(port),_sock(-1){}void init_server(void){//1. 创建套接字 --- socket// AF_INET 是一个宏值, 在这里代表着网络套接字// SOCK_DGRAM, 标定这是数据报套接字// protocol 默认情况下都是0_sock = socket(AF_INET, SOCK_DGRAM, 0);if(_sock == -1){// 套接字创建失败对于网络通信而言是致命的LogMessage(FATAL, "%s\n", "socket failed");exit(1);}//2. 绑定端口号 --- bind// bind 将相应的ip和port在内核中与指定的套接字强关联// 服务器跑起来就是一个进程, 因此需要通过// 服务器的IP + port 绑定服务器这个进程// 因此我们需要通过 sockaddr_in 设置地址信息struct sockaddr_in server;// 我们可以初始化一下这个对象// 通过bzero(), 对指定的一段内存空间做清0操作bzero(static_cast<void*>(&server), sizeof(server));// 初始化完毕后, 我们就需要填充字段// sockaddr_in 内部成员// in_port_t sin_port;  ---  对port的封装// struct in_addr sin_addr; --- 对ip的封装// sin_family  sa_family; --- 如果我们是网络套接字, 那么填充 AF_INET// 我们要知道, 0.0.0.0 这种IP地址我们称之为"点分十进制" 字符串风格的IP地址// 每个点分割的区域数值范围 [0, 255];// 四个区域代表着四个字节, 理论上标识一个IP地址, 其实四字节就足够了// 点分十进制的字符串风格的IP地址是给用户使用的// 在这里我们需要将其转成32位的整数 uint32_tserver.sin_family = AF_INET;// 当我们在网络通信时, 一方不仅要将自己的数据内容告诉对方// 还需要将自己的IP地址以及端口号告诉对方。// 即服务器的IP和端口号未来也是要发送给对方主机的特定进程(客户端进程)// 那么是不是我需要先将数据从 本地 发送到 网络呢?// 答案: 是的, 因此我们还需要注意不同主机内的大小端问题// 因此, 我们在这里统一使用网络字节序server.sin_port = htons(_port);// 而对于IP地址而言, 也是同理的// 只不过此时的IP地址是点分十进制的字符串// 因此我们需要先将其转为32位的整数, 在转化为网络字节序// 而 inet_addr() 这个接口就可以帮助我们做好这两件事//server.sin_addr.s_addr = inet_addr(_ip.c_str());// 作为 server 服务端来讲,我们不推荐绑定确定的IP,// 我们推荐采用任意IP的方案,即INADDR_ANY(是一个宏值), 本质就是((in_addr_t) 0x00000000)// 作为服务器, 我们可以不用暴露IP, 只暴露端口号即可。// INADDR_ANY让服务器,在工作过程中,可以从任意IP中获取数据// 如果我们在服务器端bind了一个固定IP, 那么此时这个服务器就只能// 收取某个具体IP的消息, 但如果我们采用INADDR_ANY// 那么就是告诉操作系统, 凡是给该主机的特定端口(_port)的数据都给我这个服务端// 有了这样的认识之后,服务端只需要端口,不需要传递IP了 (默认设置为 INADDR_ANY)server.sin_addr.s_addr = _ip.empty() ?  INADDR_ANY : inet_addr(_ip.c_str());// 填充 struct sockaddr_in done// 这里的 socklen_t 本质上就是 unsigned intsocklen_t server_addr_len = sizeof(server);if(bind(_sock, reinterpret_cast<const struct sockaddr*>(&server), server_addr_len) == -1){LogMessage(FATAL, "%s\n", "bind error");exit(2);}// 初始化doneLogMessage(NORMAL, "%s\n", "init_server success");}// 启动服务器 --- start// 第一个简单版本: echo 服务器, 客户端向服务器发送消息, 服务端原封不动的返回给客户端// 站在网络视角, 作为一款网络服务器, 永远不退出// 站在操作系统视角, 服务器本质上就是一个进程,// 因此对于这种永远不退出的进程我们也称之为常驻进程,// 永远在内存中存在, 除非系统挂了或者服务器宕机了。// 因此针对服务器我们要特别注意内存问题。绝不能内存泄露。void start(void){char buffer[SER_BUFFER_SIZE] = {0};for(;;){struct sockaddr_in client; // 这里的clientbzero(static_cast<void*>(&client), sizeof(client));socklen_t client_addr_len = sizeof(client);buffer[0] = 0;// 1. 读取客户端数据 --- recvfrom// 当服务器收到客户端发送的数据// 那么是不是服务端还需要将后续的处理结果返回给客户端呢?// 答案: 是的. 因此除了拿到数据之外, 服务端是不是还需要客户端的地址信息(IP + port)// 因此, 我们就可以理解为什么recvfrom系统调用会要后两个参数了// struct sockaddr *src_addr 是一个输出型参数, 用来获取客户端的地址信息// socklen_t *addrlen 是一个输入型参数、 输出型参数 如何理解// 输入型: 这个缓冲区 src_addr 的初始值大小,做输入型参数// 输出型: 这个缓冲区 src_addr 的实际值, 填充sockaddr_in的实际大小,做输出型参数// flags == 0 代表阻塞式的读取数据ssize_t real_read_size = recvfrom(_sock, buffer, SER_BUFFER_SIZE - 1, 0, \reinterpret_cast<struct sockaddr*>(&client), &client_addr_len);if(real_read_size > 0 /* 代表读取成功 */){// 我们就将这个数据当作字符串处理buffer[real_read_size] = 0;// 我们的目的是完成群发功能// 为了标识不同客户端进程发送的信息// 我们提取一下IP和地址// 因此未来,我们的数据信息 [客户端IP][客户端port]: info// 提取端口, 并将网络字节序 -> 主机序列uint16_t client_port = ntohs(client.sin_port);// 提取IP, IP -> 主机序列 -> 点分十进制的字符串风格// inet_ntoa 这个接口就可以帮助我们完成上面两件事std::string client_ip = inet_ntoa(client.sin_addr);// 客户端信息标志char info_sign[256] = {0};snprintf(info_sign, 256, "[%s][%d]", client_ip.c_str(), client_port);auto it = _map.find(buffer);if(it == _map.end()){LogMessage(NORMAL, "add client: %s\n", info_sign);_map[info_sign] = client;//_map.insert(std::make_pair(info_sign, client));}std::string all_data;all_data += info_sign;all_data += ": ";all_data += buffer;all_data += "\n";// 2. 向所有的客户端写回数据 --- sendtofor(const auto& it : _map){// 向每一个客户端发送消息ssize_t real_write_size = sendto(_sock, all_data.c_str(), all_data.size(), 0,\reinterpret_cast<const struct sockaddr*>(&it.second), sizeof ((it.second)));LogMessage(NORMAL, "push data [%s] to client %s\n", buffer, it.first.c_str());if(real_write_size < 0){LogMessage(ERROR, "info_sign:%s %s\n", info_sign, "write size < 0");exit(3);}}}}}~udp_server(){if(_sock != -1)close(_sock);}private:// IP地址, 这里之所以用string, 想表示为点分十进制的字符串风格的IP地址std::string _ip;// 端口号uint16_t _port;// 套接字, socket系统调用的返回值,代表返回一个新的文件描述符 int _sock;// 这个map就将客户端标志信息和相应的sockaddr_in结构关联起来std::map<std::string, struct sockaddr_in> _map;};
}#endif

3.4. demo3总结 

我们发现,当客户端进行读数据还是写数据,用的都是同一个套接字  (sock), sock代表的就是文件, 因此UDP是全双工的, 可以同时进行收发数据而不受到干扰。

而我们以前学习的管道就是半双工的。

总而言之,UDP提供了更灵活的通信方式,适用于需要快速传输、不需要建立连接的应用场景,而管道通常用于进程间通信,其中一个进程负责写入,另一个进程负责读取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1006307.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CentOS 7安装MySQL及常见问题与解决方案(含JDBC示例与错误处理)

引言 MySQL是一个流行的开源关系型数据库管理系统&#xff0c;广泛应用于各种业务场景。在CentOS 7上安装MySQL后&#xff0c;我们通常需要使用JDBC&#xff08;Java Database Connectivity&#xff09;连接MySQL进行后端操作。 目录 引言 CentOS 7安装MySQL 使用JDBC连接My…

什么时候去检测大数据信用风险比较合适?

什么时候去检测大数据信用风险比较合适?在当今这个数据驱动的时代&#xff0c;大数据信用风险检测已经成为个人的一项重要需求。本文将从贷前检测、信息泄露检测和定期检测三个方面&#xff0c;阐述何时进行大数据信用风险检测较为合适。 一、贷前检测 大数据信用风险检测在贷…

Python爬虫基础学习-互联网、HTTP与HTML

互联网或者叫国际网&#xff08;Internet&#xff09;&#xff0c;是指网络与网络之间所串连成的庞大网络&#xff0c;这些网络以一组标准的网络TCP/IP协议族相连&#xff0c;连接全世界几十亿个设备&#xff0c;形成逻辑上的单一巨大国际网络。它是由从地方到全球范围内几百万…

基于深度学习的图像去雨去雾

基于深度学习的图像去雨去雾 文末附有源码下载地址 b站视频地址&#xff1a; https://www.bilibili.com/video/BV1Jr421p7cT/ 基于深度学习的图像去雨去雾&#xff0c;使用的网络为unet&#xff0c; 网络代码&#xff1a; import torch import torch.nn as nn from torchsumm…

用户数据的FLASH存储与应用(FPGA架构)

该系列为神经网络硬件加速器应用中涉及的模块接口部分&#xff0c;随手记录&#xff0c;以免时间久了遗忘。 一 背景 我们知道&#xff0c;在FPGA做神经网络应用加速时&#xff0c;涉及到权重参数的存储和加载。通常在推理过程中&#xff0c;会将权重参数存储在外部DDR或片上S…

Voip测试工具

SIPp是一个测试SIP协议性能的工具软件。这是一个GPL的开放源码软件。 sipp是安装在linux机器上的 SIPp可以用来测试许多真实的SIP设备&#xff0c;如SIP代理&#xff0c;B2BUAs,SIP媒体服务器&#xff0c;SIP/x网关&#xff0c;SIP PBX&#xff0c;等等&#xff0c;它也可以模…

月薪15000住家阿姨的一天 网友:这是她应得的

大家好&#xff01; 我是老洪。 刚刚浏览到一则关于“月薪15000住家阿姨的一天”的新闻&#xff0c;内容颇为引人关注。 从网友们的反应来看&#xff0c;大多数人都认为这位阿姨身兼多职&#xff0c;包括厨师、幼师、理发师等角色&#xff0c;所以她所得的月薪是应得的。 毋庸置…

有来团队后台项目-解析7

sass 安装 因为在使用vite 创建项目的时候,已经安装了sass,所以不需要安装。 如果要安装,那么就执行 npm i -D sass 创建文件 src 目录下创建文件 目录结构如图所示: reset.scss *, ::before, ::after {box-sizing: border-box;border-color: currentcolor;border-st…

Linux - 安装 nacos(详细教程)

目录 一、简介二、安装前准备三、下载与安装四、基本配置五、单机模式 一、简介 官网&#xff1a;https://nacos.io/ GitHub&#xff1a;https://github.com/alibaba/nacos Nacos 是阿里巴巴推出的一个新开源项目&#xff0c;它主要是一个更易于构建云原生应用的动态服务发现…

母婴类目电商平台数据分析

母婴产品涉及丰富&#xff0c;以前&#xff0c;奶粉、纸尿裤这类产品基本代表了整体母婴市场中的消费品&#xff0c;现如今&#xff0c;母婴产品还包含孕妇产品、待产护理等类型的产品&#xff0c;而如今&#xff0c;随着母婴行业的高速发展和消费升级&#xff0c;母婴商品的种…

Linux网络套接字之UDP网络程序

(&#xff61;&#xff65;∀&#xff65;)&#xff89;&#xff9e;嗨&#xff01;你好这里是ky233的主页&#xff1a;这里是ky233的主页&#xff0c;欢迎光临~https://blog.csdn.net/ky233?typeblog 点个关注不迷路⌯▾⌯ 实现一个简单的对话发消息的功能&#xff01; 目录…

mybatis-编写mapper.xml SQL语句时无提示

你们好&#xff0c;我是金金金。 场景 可以看见sql颜色都是白色的&#xff0c;而且编写的时候没有提示&#xff0c;简直痛苦 排查 中途有设置过SQL方言等&#xff0c;都没有解决我的问题 解决 很简单&#xff0c;https 改成 http 就有提示了&#xff01;&#xff01;&#x…

Docker与Nacos的下载与安装配置

文章目录 docker作用docker的下载nacos 下载1. 首先搜索需要的下载2. 拉取stars最多的即可3. 启动nacos4. 打开防火墙8848端口5. 访问nacos docker 作用 Docker 是一种开源的容器化平台&#xff0c;它的作用主要包括以下几个方面&#xff1a; 应用程序的打包和分发&#xff1…

蓝牙系列十三:协议栈L2CAP层

L2CAP 全称为逻辑链路控制与适配协议(Logical Link Control and Adaptation Protocol)&#xff0c;位于基带层之上&#xff0c;将基带层的数据分组交换为便于高层应用的数据分组格式&#xff0c;并提供协议复用和服务质量交换等功能。 该层属于主机的内容&#xff0c;位于HCI层…

割点原理及封装好的割点类

作者推荐 视频算法专题 预备知识 本分析针对&#xff1a;连通无向图G。 搜索树 节点的父子关系&#xff1a;任意 节点的邻接 节点除了已处理 节点&#xff0c;都是它的子 节点。 以任意一点为根开始DFS&#xff0c;计算所有 节点的父子关系。只保留个子 节点到父 节点形成…

【实战项目】Boost搜索引擎项目

目录 1. 项目的相关背景 2. 搜索引擎的相关宏观原理 3. 搜索引擎技术栈和项目环境 4. 正排索引 vs 倒排索引 - 搜索引擎具体原理 4.1 正排索引 4.2 目标文档进行分词 4.3 倒排索引 4.4 模拟一次查找的过程&#xff1a; 5. 编写数据去标签与数据清洗的模块 Parser 5.1…

力扣串题:字符串中的第一个唯一字母

映射做法&#xff1a;将字母转为数字之类的转化必须在运算中实现如-a int firstUniqChar(char * s){int a[26] {0};int len strlen(s);int i;for (i 0; i < len; i)a[s[i] - a];for (i 0; i < len; i) {if (a[s[i] - a] 1)return i;}return -1; }

第42期 | GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

Python学习笔记-Flask实现简单的投票程序

1.导入flask包 from flask import Flask,jsonify,abort,make_response,request,render_template 2.初始化 Flask 应用: app Flask(__name__) 3. 定义投票种类 data [{id:0,name:劳动节,num:0},{id:1,name:国庆节,num:0},{id:2,name:春节,num:0} ] 4.app.route(/index): …

FPGA高端项目:FPGA基于GS2971+GS2972架构的SDI视频收发+HLS图像缩放+多路视频拼接,提供4套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本方案的SDI接收发送本方案的SDI接收图像缩放应用本方案的SDI接收纯verilog图像缩放纯verilog多路视频拼接应用本方案的SDI接收OSD动态字符叠加输出应用本方案的SDI接收HLS多路视频融合叠加应用本方案的SDI接收G…