目录
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*, 读取管道内容 (执行命令的结果)。
- command:需要执行的 shell 命令或进程名。
- mode:打开管道的模式,可以是 " r "(读模式)或 " w "(写模式)。
- 若模式为 " r ",则返回可用于读取管道输出流的 FILE 指针。
- 若模式为 " w ",则返回可用于向管道输入流写入数据的 FILE 指针。
在使用完毕后,必须使用 pclose() 函数来关闭由 popen() 函数打开的管道,并回收相关资源 (等等子进程退出,回收子进程资源),以避免出现资源泄露的情况。
pclose() 函数将阻塞调用进程,直到被调用进程终止并关闭它所打开的管道。
popen() 和 pclose() 函数的结合机制类似于 Linux 系统上的 shell 命令中管道(|)的功能,即将一个进程的输出连接到另一个进程的输入,可以方便地实现进程间的通信。
1.2. strcasestr 接口
#include <string.h>
char *strcasestr(const char *haystack, const char *needle);
strcasestr 函数用于在一个字符串中查找另一个字符串,并返回第一次出现的匹配子串的指针,不区分大小写。
其参数如下:
- haystack:要搜索的字符串,即被查找的字符串。
- 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提供了更灵活的通信方式,适用于需要快速传输、不需要建立连接的应用场景,而管道通常用于进程间通信,其中一个进程负责写入,另一个进程负责读取。