目录
接上篇套接字编程 --- 一,继续。
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 1024
void 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_family
server.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 1024
namespace Xq
{
class udp_server
{
public:
// 需要显示传递服务器的 port
udp_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_t
server.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 int
socklen_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 成功
// 初始化done
LogMessage(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_message
ssize_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; //线程ID
thread_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_family
server.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 1024
namespace Xq
{
class udp_server
{
public:
// 需要显示传递服务器的 port
udp_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_t
server.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 int
socklen_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);
}
// 初始化done
LogMessage(NORMAL, "%s\n", "init_server success");
}
// 启动服务器 --- start
// 第一个简单版本: echo 服务器, 客户端向服务器发送消息, 服务端原封不动的返回给客户端
// 站在网络视角, 作为一款网络服务器, 永远不退出
// 站在操作系统视角, 服务器本质上就是一个进程,
// 因此对于这种永远不退出的进程我们也称之为常驻进程,
// 永远在内存中存在, 除非系统挂了或者服务器宕机了。
// 因此针对服务器我们要特别注意内存问题。绝不能内存泄露。
void start(void)
{
char buffer[SER_BUFFER_SIZE] = {0};
for(;;)
{
struct sockaddr_in client; // 这里的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;
// 我们的目的是完成群发功能
// 为了标识不同客户端进程发送的信息
// 我们提取一下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. 向所有的客户端写回数据 --- sendto
for(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提供了更灵活的通信方式,适用于需要快速传输、不需要建立连接的应用场景,而管道通常用于进程间通信,其中一个进程负责写入,另一个进程负责读取。