UDP网络程序

上一章中,我们介绍了socket,以及TCP/UDP协议。这一章带大家实现几个UDP协议的网络服务。我们需要一个 服务端和一个客户端。

1.服务端实现

1.1socket函数

#include <sys/types.h>     
#include <sys/socket.h>int socket(int domain, int type, int protocol);

参数说明:

  • domain:域,标识了这个套接字的通信类型(网络通信/本地通信),这个就是sockaddr结构体的前16个bit位。如果是本地通信就是AF_UNIX,如果是网络通信就是AF_INET。 
  • type:套接字提供的服务类型,常见的就是SOCK_STREAMSOCK_DGRAM,如果我们是基于TCP协议的通信,就使用SOCK_STREAM,表示的是流式服务。如果我们是基于UDP协议通信的,就使用SOCK_DGRAM,表示的是数据包服务。
  • protocol:创建套接字的类型(TCP/UDP),但是这个参数可以由前两个参数决定。所以通常设置为0

返回值:

成功会返回一个文件描述符,失败返回-1,错误码被设置

在系统的文件操作中,也会返回文件描述符,与socket返回的文件描述符不同的是,普通文件的文件缓冲区对应的是磁盘,而socket返回的文件描述符的文件缓冲区对应的是网卡。用户将数据写到缓冲区,由操作系统自动将缓冲区中的数据刷新到网卡中,网卡会负责将这个数据发送到对端主机上。

class UdpServer
{
public:UdpServer(){// 创建套接字_socket = socket(AF_INET, SOCK_DGRAM, 0);if (_socket < 0){std::cerr << "create socket error" << std::endl;exit(1);}std::cout << "create socket success, socket: " << _socket << std::endl;}~UdpServer(){close(_socket);}private:int _socket;
};

调用socket函数,就能创建一个套接字了,第一个参数我们填AF_INET,表示的是我们是网络通信。第二个参数我们填SOCK_DGRAM,表示我们是UDP服务(数据报)。由于一个进程启动时,默认会打开标准输入,标准输出,标准错误这三个文件描述符,而文件描述符的创建规则就是从0开始,向上找到第一个没有使用的,所以我们可以猜测以下_sock的值为3.


1.2bind函数

我们创建完套接字以后,也只是打开了一个文件。还没有将这个文件和网络关联起来,所以我们需要使用bind函数,将IP+port和文件绑定

#include <sys/types.h>     
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数介绍:

  • sockfd:socket函数的返回值。
  • addr:通用结构体,包括协议家族,IP地址,端口号port
  • addrlen:addr的长度

返回值介绍:

成功返回0,错误返回-1

sockaddr_in结构体

struct sockaddr_in{short int sin_family;in_port_t sin_port;			struct in_addr sin_addr;	unsigned char sin_zero[8];};
  • sin_family:协议家族,表示通信类型(AF_INET网络通信,PF_INET本地通信)
  • sin_port:端口号(网络字节序)
  • sin_addr:IP地址。
  • sin_zero:填充字段,让sizeof(sockaddr_in) = 16

既然第二步需要添加IP和端口号,所以我们要对代码修改一下,给UDP的构造函数添加port和str(IP的点分十进制表示形式)

class UdpServer
{
public:UdpServer(const uint16_t &port, const std::string &str = ""){// 创建套接字_socket = socket(AF_INET, SOCK_DGRAM, 0);if (_socket < 0){std::cerr << "create socket error" << std::endl;exit(1);}std::cout << "create socket success, socket: " << _socket << std::endl;sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(str.c_str());int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));if (n < 0){std::cout << "bind error" << std::endl;exit(2);}std::cout << "bind success " << std::endl;}~UdpServer(){close(_socket);}private:int _socket;
};

需要注意的是在填写sockaddr_in的参数的时候,

  • sin_family需要和socket套接字创建时的domain相同。
  • 由于端口号和IP将来是要发送到网络的,而网络数据流统一采用大端的形式,所以为了代码的可移植性,我们不管自己是大端还是小端,统一调用函数hton转化成大端。
  • 对于IP来说,如果直接发送的是点分十进制形式如(192.168.12.80)的形式,则需要占用过多的字节数,对网络传输无疑是一种很大的消耗,所以我们采用一个32位的整数来表示IP,在网络传输中,要将点分十进制的IP转化成整数,再进行传输。
#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); //点分十进制转整数in_addr_t inet_network(const char *cp);char *inet_ntoa(struct in_addr in); //整数转点分十进制struct in_addr inet_makeaddr(int net, int host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);

这里重点学习两个,inet_addr和inet_ntoa。


1.3运行服务器

首先我们要知道,服务器就是一个死循环,在启动之后永远不会退出。

#include <iostream>
#include <memory>
#include "UdpServer.hpp"void Usage()
{std::cout << "Please enter:  ./UdpServer [ip] port" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 3){Usage();return 3;}std::string ip = argv[1];uint16_t port = atoi(argv[argc - 1]);std::unique_ptr<UdpServer> p(new UdpServer(port, ip));p->Start();return 0;
}

我们可以使用命令行参数的形式,在启动这个程序时就把IP和端口号传递过去。


1.4IP的绑定

ifconfig操作系统中用于配置和显示网络接口参数的命令行工具。它主要用于查看和设置网络接口(如以太网和 Wi-Fi)的详细信息,包括 IP 地址、子网掩码、广播地址、MAC 地址等。

这里的127.0.0.1表示的时本地环回,用于在本地进行测试,如果本地测试成功,将来网络测试出现问题,那么大概率就网络的问题。本地换回顾名思义就是只会在本地不会发送到网络当中。

当我们运行上面的程序的时候,也可以时netstat函数查看网络情况,

  • -a或--all 显示所有连线中的Socket。
  • -l或--listening 显示监控中的服务器的Socket。
  • -n或--直接使用IP地址(数字),而不是域名。
  • -p或--显示正在使用Socket的程序识别码和程序名称。
  • -t或--tcp 显示TCP传输协议的连线状况。
  • -u或--udp 显示UDP传输协议的连线状况。

我们可以看到确实能看到我们刚刚运行的程序成功bind了一个IP和端口号。

  • 但是在实际情况下,服务器时不建议绑定一个固定IP的。
  • 安全性:攻击者可能会针对这个固定的IP地址进行攻击,比如DDoS攻击或者其他类型的网络攻击。相比之下,使用动态IP或者负载均衡等技术可以更好地分散攻击,提高系统的安全性。
  • 可用性:在某些情况下,固定IP可能并不总是可用的。例如,在云服务环境中,IP地址可能会因为服务器的迁移或重新部署而发生变化。如果服务端绑定到这样的固定IP,当IP地址发生变化时,服务端可能无法正常工作,导致服务中断。
  • 一个服务器可能会有多个IP(多张网卡),当客户端发送数据时,每张网卡都会收到该数据,如果我们想访问指定的某一个端口8080,并且如果指定了IP,那么只能由那一个指定的IP接受数据,但是如果我们绑定的是任意IP,那么只要是发送给8080端口的,任意一个网卡接受到了都会向上交付给服务器。

 addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());

所以我们修改一下,如果创建服务端的时候,传了IP,那就用传的IP,如果没用,那就用我们INADDR_ANY其实就是(0.0.0.0),绑定之后,只要是发送到这台主机上,端口号为XXX(我这里是8080)的,就将数据全部交给这个进程。

1.5读取数据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:读len个字节
  • flags:读取方式,0表示阻塞读取
  • src_addr:发送端的信息
  • addrlen:输出型参数,表示src_addr的长度(必须初始化为sizeof(src_addr))
void Start(){while (true){char temp[1024];sockaddr_in addr;socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){printf("[%s:%d]# %s", ip.c_str(), port, temp);}}}

src_addr用来保存发送端的信息,我们在前面使用bind函数的时候,也介绍过sockaddr结构,里面包含了发送端的IP和端口port,但是因为是网络序列,我们需要将他转化成主机序列,所以使用ntoh函数,ip是32位整数,我们想转化成点分十进制形式方便观看,于是可以使用inet_ntoa函数。

1.6服务端整体代码

#pragma once#include <iostream>
#include <string>
#include <unordered_set>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpServer
{
public:UdpServer(const uint16_t &port, const std::string &str = ""){// 创建套接字_socket = socket(AF_INET, SOCK_DGRAM, 0);if (_socket < 0){std::cerr << "create socket error" << std::endl;exit(1);}std::cout << "create socket success, socket: " << _socket << std::endl;sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));if (n < 0){std::cout << "bind error" << std::endl;exit(2);}std::cout << "bind success " << std::endl;}void Start(){while (true){char temp[1024];sockaddr_in addr;socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){printf("[%s:%d]# %s", ip.c_str(), port, temp);}}}~UdpServer(){close(_socket);}private:int _socket;
};

2.客户端实现

服务端的创建和客户端类似。我们也需要创建套接字

2.1创建套接字

void Usage()
{std::cout << "Please enter:  ./Client [ip] port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();return 3;}std::string ip = argv[1];uint16_t port = atoi(argv[argc - 1]);// 创建套接字int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);if (clientsocket < 0){std::cerr << "create socket error" << std::endl;exit(1);}std::cout << "create socket success, socket: " << clientsocket << std::endl;return 0;
}

2.1绑定问题

客户端必须绑定IP和端口,但是不用显示的bind

这句话是什么意思呢,就是我们需要将网卡文件和IP+端口进行绑定,但是不需要我们自己手动绑定,操作系统会自动帮我们绑定一个端口号。

为什么不能显示绑定呢,首先我们要知道,一个端口只能对应一个进程,现在有两家公司,各推出了一款客户端,如果他们想让端口不冲突,那就不能让自己家的客户端绑定的端口和对方的一样,如果一样就会起冲突,但是互联网上有很多家公司,难道都要协商一下,哪个端口谁来用吗。所以我们采用了让操作系统帮我们自动分配一个没有使用的端口号。

当我们调用了类似与sendto(发送信息)的函数的时候,操作系统会帮我们自动分配一个端口号,也就是说,客户端每次启动时的端口号可能都不相同。

  • 服务端的端口号为什么是固定的

服务端的端口号是众所周知的,如果每次都不一样的话,客户端就找不到服务器了

2.3发送信息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:对端主机的信息(包括协议家族,IP,port)
  • addrlen:表示src_addr的长度

参数和recvfrom类似

// 直接给服务器send,系统会自动帮我们bindsockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip.c_str());while (true){std::string buffer;std::cout << "Please enter# ";std::getline(std::cin, buffer);sendto(clientsocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));}

当我们将客户端发送消息的逻辑 完成之后就可以正常开始通信了。

我们先启动服务端,再启动客户端(两个不同的进程)

命令行中提示我们输入

当我们在右边客户端输入的消息之后,就能成功发送给服务端了。当我们再次使用netstat查看网络情况

当我们再次使用netstat查看网络情况,就可以看到客户端和服务端正在运行,并且能看到各自的端口号。

这样我们就实现了一个简单的服务端客户端模型,下面是源码:

2.4源码

UdpServer.hpp

//Server.hpp#pragma once#include <iostream>
#include <string>
#include <unordered_set>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpServer
{
public:UdpServer(const uint16_t &port, const std::string &str = ""){// 创建套接字_socket = socket(AF_INET, SOCK_DGRAM, 0);if (_socket < 0){std::cerr << "create socket error" << std::endl;exit(1);}std::cout << "create socket success, socket: " << _socket << std::endl;sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));if (n < 0){std::cout << "bind error" << std::endl;exit(2);}std::cout << "bind success " << std::endl;}void Start(){while (true){char temp[1024];sockaddr_in addr;socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){temp[n] = 0;printf("[%s:%d]# %s\n", ip.c_str(), port, temp);}}}~UdpServer(){close(_socket);}private:int _socket;
};

Server.cc

#include <iostream>
#include <memory>
#include "UdpServer.hpp"void Usage()
{std::cout << "Please enter:  ./UdpServer [ip] port" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 3 && argc != 2){Usage();return 3;}std::string ip = argv[1];uint16_t port = atoi(argv[argc - 1]);if (argc == 2)ip = "";std::unique_ptr<UdpServer> p(new UdpServer(port, ip));p->Start();return 0;
}

Client.cc

#include <iostream>
#include <memory>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>void Usage()
{std::cout << "Please enter:  ./Client [ip] port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();return 3;}std::string ip = argv[1];uint16_t port = atoi(argv[argc - 1]);// 创建套接字int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);if (clientsocket < 0){std::cerr << "create socket error" << std::endl;exit(1);}std::cout << "create socket success, socket: " << clientsocket << std::endl;// 直接给服务器send,系统会自动帮我们bindsockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip.c_str());while (true){std::string buffer;std::cout << "Please enter# ";std::getline(std::cin, buffer);sendto(clientsocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));}return 0;
}

3.更多功能

在前面我们学习了最基础的服务端客户端编写,让客户端给服务端发消息,服务端直接显示,我们可以让服务端在拿到数据后,对数据进行特殊处理。

    void ExecuteCommand(){while (true){char temp[1024];sockaddr_in addr;memset(&addr, 0, sizeof(addr));socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){// 接收到数据temp[n] = '\0';std::string buffer = temp;// 对数据做处理// 将数据写回sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));}}}

我们可以设计出这样一个函数,就是前面的Start,但是在拿到数据后,先将数据进行处理,再将数据发送给客户端。我们下面要完成的就是这个对数据处理的函数。

3.1回显echo

很简单,我们收到用户的数据之后,并不需要处理直接返回即可

void ExecuteCommand(){while (true){char temp[1024];sockaddr_in addr;memset(&addr, 0, sizeof(addr));socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){// 接收到数据temp[n] = '\0';std::string buffer = temp;// 对数据做处理EchoMessage(buffer);// 将数据写回sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));}}}void EchoMessage(std::string &buffer){return;}

注意,这里的EchoMessage函数虽然什么都没有做,但是,为了突出用户发的信息是被处理过的,我们还是添加了一个函数。

也是可以成功运行。

3.2大写转换

将客户端发送的小写字母转化成大写字母

    void ExecuteCommand(){while (true){char temp[1024];sockaddr_in addr;memset(&addr, 0, sizeof(addr));socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){// 接收到数据temp[n] = '\0';std::string buffer = temp;// 对数据做处理//EchoMessage(buffer);Transformed(buffer);// 将数据写回sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));}}}void Transformed(std::string &buffer){for (auto &ch : buffer){if ('a' <= ch && ch <= 'z'){ch -= 32;}}return;}

我们只需要将调用的函数替换一下,就能完成不同的业务。

3.3英译汉词典

将用户发送的英文转化成汉语

我们先创建一个哈希表,保存英语单词以及对应的意思

static std::unordered_map<std::string, std::string> Dict;void TranslationInit()
{Dict.insert({"apple", "苹果"});Dict.insert({"pear", "梨子"});Dict.insert({"banana", "香蕉"});Dict.insert({"orange", "橘子"});Dict.insert({"left", "左"});Dict.insert({"right", "右"});Dict.insert({"sun", "太阳"});Dict.insert({"moon", "月亮"});
}
    void ExecuteCommand(){while (true){char temp[1024];sockaddr_in addr;memset(&addr, 0, sizeof(addr));socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);std::string ip = inet_ntoa(addr.sin_addr);uint16_t port = ntohs(addr.sin_port);if (n > 0){// 接收到数据temp[n] = '\0';std::string buffer = temp;// 对数据做处理//EchoMessage(buffer);//Transformed(buffer);Translation(buffer);// 将数据写回sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));}}}void Translation(std::string &buffer){auto it = Dict.find(buffer);if (it == Dict.end()){buffer = "not find";}else {buffer = it->second;}}

3.4执行命令

执行用户发送的shell命令,例如pwd,ls等。

我们介绍一个函数

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

popen函数会fork创建子进程,并且让子进程程序替换执行command命令,最终把结果写到一个文件当中。type就是以什么方式打开这个文件(w/r/a)。

std::unordered_set<std::string> forbid = {"rm","mv","kill","cp"
};void ExecuteCommand(std::string &buffer)
{// 查询有无禁止命令if (!sercharforbid(buffer)){buffer = "you can't do that\n";return;}// 使用popen执行用户命令,并将结果写入fp中if (buffer == "ll"){buffer = "ls -l --color=auto";}FILE *fp = popen(buffer.c_str(), "r");if (fp == nullptr){buffer = "command is unknow";return;}// 读取信息buffer.clear();char temp[1024];while (fgets(temp, sizeof(temp), fp) != NULL){buffer += temp;}pclose(fp);
}bool sercharforbid(const std::string &buffer)
{for (auto com : forbid){int pos = buffer.find(com);if (pos != std::string::npos){return false;}}return true;
}

我们将一些命令保存在哈希桶中,并且不让用户执行这些命令,例如rm删除之类。

3.5网络聊天室

将来会有很多用户加入这个聊天室,一个用户发送消息,能让其他用户都看到这条消息。

我们先创建一个类,这个类对sockaddr_in进行封装。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{
public:InetAddr(const sockaddr_in addr): _addr(addr){_ip = inet_ntoa(addr.sin_addr);_port = ntohs(addr.sin_port);}std::string GetUser(){std::string temp;temp += _ip;temp += " : ";temp += std::to_string(_port);return temp;}std::string& GetIp(){return _ip;}uint16_t GetPort(){return _port;}sockaddr_in& GetAddr(){return _addr;}private:std::string _ip;uint16_t _port;sockaddr_in _addr;
};

这个类会提取出sockaddr_in中的IP和端口并保存,GetUser函数会返回一个IP+端口号的字符串。

我们再创建一个保存用户信息的哈希桶

std::unordered_map<std::string, InetAddr> _users;void AddUser(InetAddr &user)
{std::string userMessage = user.GetUser();if (_users.find(userMessage) != _users.end()){return;}_users.insert({userMessage, user});
}

每当有用户发消息时,我们根据用户的sockaddr_in就能提取出这个用户的IP+端口,如果这个用户是第一次发消息,我们就把他的信息保存起来。

void Route(size_t sock, std::string message)
{//将message发送给每一个用户for (auto user : _users){sockaddr_in addr = user.second.GetAddr();sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&addr, sizeof(addr));}
}

最后再根据哈希桶中保存的用户信息,就能再将数据发送给每一个用户了。

我们就完成了一个简单的网络聊天室,但是我们通过实验会发现还存在很大的问题,由于我们的服务端是单线程阻塞式读取,所以当别人发送数据的时候,我们可能正在阻塞,并不会显示数据,只有在写之后,数据才会重新打印出来。

上面我们的测试也能反应这一点,我们的执行顺序是,从上至下从左至右,每次发一条消息,一共两次。可以看到第二次左上那个进程才收到了右上第一次发的消息,这很显然不复合实际中的网络通信。

所以我们需要将服务端改成线程池版本,服务端改成两个线程(一个读一个写)

#pragma once#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <functional>
#include <unistd.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "LogMessage.hpp"
#include "ExistReason.hpp"
#include "ThreadPool.hpp"
#include "LocalGuard.hpp"
#include "Pthread.hpp"
#include "InetAddr.hpp"using task_t = std::function<void()>;class ChatServer
{
public:ChatServer(const uint16_t &port, const std::string &str = ""){// 创建套接字_socket = socket(AF_INET, SOCK_DGRAM, 0);if (_socket < 0){Log::LogMessage(Error, "create socket error");exit(CREATE_SOCKET_ERROR);}Log::LogMessage(Debug, "create socket success, socket: %d", _socket);// 进行bindsockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));if (n < 0){Log::LogMessage(Error, "bind error");exit(BIND_ERROR);}Log::LogMessage(Debug, "bind success");pthread_mutex_init(&_user_mutex, nullptr);ThreadPool<task_t>::GetInstance()->Start();}~ChatServer(){pthread_mutex_destroy(&_user_mutex);close(_socket);}void Start(){while (true){char temp[1024];sockaddr_in addr;memset(&addr, 0, sizeof(addr));socklen_t addrlen = sizeof(addr);int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);temp[n] = 0;InetAddr user(addr);if (n > 0){// 将用户信息保存起来AddUser(user);std::string message = "[";message += user.GetUser();message += "] ";message += temp;// 将任务push到队列当中task_t task = std::bind(&ChatServer::Route, this, _socket, message);ThreadPool<task_t>::GetInstance()->Push(task);}else if (n == 0){// 对端关闭连接Log::LogMessage(Debug, "close connection");}else{Log::LogMessage(Warning, "server recvfrom warning");}}}private:void AddUser(InetAddr &user){LockGuard lock(&_user_mutex);std::string userMessage = user.GetUser();if (_users.find(userMessage) != _users.end()){return;}_users.insert({userMessage, user});}void Route(size_t sock, std::string message){//将message发送给每一个用户LockGuard lock(&_user_mutex);for (auto user : _users){sockaddr_in addr = user.second.GetAddr();sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&addr, sizeof(addr));}}private:int _socket;std::unordered_map<std::string, InetAddr> _users;pthread_mutex_t _user_mutex;
};

我们使用线程池,提前创建好几个线程,将来只要有一个用户发消息了,就指派一个线程去处理数据(将这个信息发送给其他用户)。

客户端

#include <iostream>
#include <memory>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "LogMessage.hpp"
#include "ExistReason.hpp"
#include "Pthread.hpp"void Usage()
{std::cout << "Please enter:  ./Client [ip] port" << std::endl;
}struct ThreadDate
{ThreadDate(int sock, sockaddr_in addr): _sock(sock), _addr(addr){}int _sock;sockaddr_in _addr;
};void Sender(ThreadDate date)
{while (true){std::string buffer;std::cout << "Please enter# ";std::getline(std::cin, buffer);sendto(date._sock, buffer.c_str(), buffer.size(), 0, (sockaddr *)&date._addr, sizeof(date._addr));if (sendto <= 0){std::cerr << "send error" << std::endl;}}
}void Recver(ThreadDate date)
{char mes[1024];while (true){sockaddr_in add;socklen_t addlen = sizeof(add);int n = recvfrom(date._sock, mes, sizeof(mes) - 1, 0, (sockaddr *)&add, &addlen);if (n > 0){mes[n] = '\0';std::cerr << mes << std::endl;}}
}int main(int argc, char *argv[])
{if (argc != 3){Usage();return USE_ERROR_MANUAL;}std::string ip = argv[1];uint16_t port = atoi(argv[argc - 1]);// 创建套接字int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);if (clientsocket < 0){exit(1);}// 直接给服务器send,系统会自动帮我们bindsockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip.c_str());ThreadDate date(clientsocket, addr);Thread<void, ThreadDate> sender(Sender, date);Thread<void, ThreadDate> recver(Recver, date);sender.Create();recver.Create();sender.Jion();recver.Jion();return 0;
}

主进程创建两个线程,一个线程进行等待,一个线程发送数据。

最终我们发现就解决了之前的问题,但是这样又有了新的问题,由于是多进程,向屏幕打印时会有出错。我们可以使用管道,注意一个细节,客户端的收消息进程在收到消息时打印使用的是cerr,我们只需要将标准错误(2号文件描述符)重定向到管道文件中,就能成功将读写分离。

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

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

相关文章

Linux——十个槽位,RWX

Linux——RWX 十个槽位 - 表示文件 d 表示文件夹 l 表示软链接 r权&#xff0c;针对文件可以查看文件内容 针对文件夹&#xff0c;可以查看文件夹内容&#xff0c;如ls命令 w权&#xff0c;针对表示可以修改此文件 针对文件夹&#xff0c;可以在文件夹内&#…

Qt案例 通过调用Setupapi.h库实现对设备管理器中设备默认驱动的备份

参考腾讯电脑管家-软件市场中的驱动备份专家写的一个驱动备份软件案例&#xff0c;学习Setupapi.h库中的函数使用.通过Setupapi.h库读取设备管理器中安装的设备获取安装的驱动列表&#xff0c;通过bit7z库备份驱动目录下的所有文件. 目录导读 实现效果相关内容示例获取SP_DRVIN…

Scikit-learn 快速入门篇

Sklearn 简介 scikit-learn (sklearn) 是 Python 中用于机器学习的最流行的库之一。它提供了一系列有效的算法和工具&#xff0c;涵盖各种机器学习任务&#xff0c;包括&#xff1a; 分类回归聚类降维模型选择数据预处理 Sklearn 六大模块 分类&#xff1a;预测离散类别 算…

分布式强化学习

标题 易混淆概念联邦学习与强化学习1&#xff09;联邦学习应用于强化学习2&#xff09;强化学习应用于联邦学习 时空图卷积网络&#xff08;ST-GCN&#xff09;基本概念结合训练 易混淆概念 DistributionalRL是分布RL&#xff0c;不是分布式RL。分布RL是把Q值从一个期望构建成…

2024-4-10 群讨论:JFR 热点方法采样实现原理

以下来自本人拉的一个关于 Java 技术的讨论群。关注公众号&#xff1a;hashcon&#xff0c;私信拉你 什么是 JFR 热点方法采样&#xff0c;效果是什么样子&#xff1f; 其实对应的就是 jdk.ExecutionSample 和 jdk.NativeMethodSample 事件 这两个事件是用来采样的&#xff0c…

睿尔曼复合机器人之底盘操作流程

以操作流程为例&#xff0c;介绍底盘的操作流程。 开机&#xff1a;长按电源按钮&#xff0c;蜂鸣器短响两声&#xff0c;当第三声变长鸣后松开&#xff0c;等待机器开机。 使用&#xff1a; 建立通讯&#xff1a;主要采用无线WiFi与底盘进行通讯连接 无线连接方式&#xff…

副业天花板流量卡推广,小白也可轻松操作

在如今的互联网时代&#xff0c;手机已经不仅仅是一款工具&#xff0c;更像是我们生活中的一部分&#xff0c;那么手机卡也是必需品&#xff0c;但存在的问题就是:很多手机卡的月租很贵&#xff0c;流量也不够用。所以大家都在寻找一个月租低&#xff0c;流量多的卡&#xff0c…

计算机网络—HTTPS协议详解:工作原理、安全性及应用实践

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;ヒューマノイド—ずっと真夜中でいいのに。 1:03━━━━━━️&#x1f49f;──────── 5:06 &#x1f504; ◀️ ⏸…

NC251500 coin

链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 题目背景 假如我那时握住的不是硬币&#xff0c;而是 ... 题意简述 Rikka 和 Yuuta 在玩游戏&#xff0c;每一次他们会抛一枚硬币&#xff0c;正面向上的概率是 p&#xff0c;反面向上的概率是…

C++设计模式:享元模式(十一)

1、定义与动机 概述&#xff1a;享元模式和单例模式一样&#xff0c;都是为了解决程序的性能问题。面向对象很好地解决了"抽象"的问题&#xff0c;但是必不可免得要付出一定的代价。对于通常情况来讲&#xff0c;面向对象的成本大豆可以忽略不计。但是某些情况&#…

NAPI 类对象导出及其生命周期管理(下)

4. 样例工程源码剖析 工程的模板是Native C,模型是Stage。源码剖析主要围绕以下几个文件 4.1. NAPI导出对象和生命周期管理具体实现 4.1.1. 定义NapiTest类及方法 Napi.h文件内容如下&#xff1a; #ifndef __NAPI_TEST_H__ #define __NAPI_TEST_H__#include "napi/nat…

数据集学习

1&#xff0c;CIFAR-10数据集 CIFAR-10数据集由10个类的60000个32x32彩色图像组成&#xff0c;每个类有6000个图像。有50000个训练图像和10000个测试图像。 数据集分为五个训练批次和一个测试批次&#xff0c;每个批次有10000个图像。测试批次包含来自每个类别的恰好1000个随机…

【科技】2024最新微信机器人一键部署教程

外话 话说上次写文章好像又过了几个月了…… 其实还是因为马上小升初的各种密考&#xff0c;其它地方不知道&#xff0c;反正广东这块名校基本上都得密考考进去 笔者连考几次都惨不忍睹…… 不过5月份会有一个信息技术特长生招生&#xff0c;看看能不能吧~ 正文 先说&#xff…

第四百五十五回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 使用方法 3. 内容总结 我们在上一章回中介绍了"overlay_tooltip用法"相关的内容&#xff0c;本章回中将介绍onBoarding包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中介绍的onBo…

流动人员人事档案管理信息系统

流动人员人事档案管理信息系统是一种用于管理流动人员的人事档案的信息系统。该系统可以对流动人员的基本信息、工作经历、学历教育、培训记录、奖惩记录等进行管理和统计。通过该系统&#xff0c;可以方便地查询和维护流动人员的人事档案信息&#xff0c;提高人力资源管理的效…

核心api实操-Activiti7从入门到专家(5)

背景 上一节已经搭建了&#xff0c;具体的开发环境&#xff0c;数据库&#xff0c;并且找了一个可以用bpmnjs流程设计器&#xff0c;这一些&#xff0c;我们对核心api做个基础的实操&#xff0c;有个感性的认知&#xff0c;另外对数据库和基本数据流动有个理解。 部署 模板部…

从零自制docker-9-【管道实现run进程和init进程传参】

文章目录 命令行中输入参数长度过长匿名管道从父进程到子进程传参[]*os.File{}os.NewFile和io.ReadAllexe.LookPathsyscall.Execstrings.Split(msgStr, " ")/bin/ls: cannot access : No such file or directory代码 命令行中输入参数长度过长 用户输入参数过长或包…

CSS基础:border-radius圆角边框的4种写法规则以及网页实战应用的3个场景

你好&#xff0c;我是云桃桃。 一个希望帮助更多朋友快速入门 WEB 前端的程序媛。大专生&#xff0c;一枚程序媛&#xff0c;感谢关注。回复 “前端基础题”&#xff0c;可免费获得前端基础 100 题汇总&#xff0c;回复 “前端工具”&#xff0c;可获取 Web 开发工具合集 265篇…

SpringBoot 中的日志原来是这么工作的

在有些场景&#xff0c;能通过调整日志的打印策略来提升我们的系统吞吐量,你知道吗&#xff1f; 我们以Springboot集成Log4j2为例&#xff0c;详细说明Springboot框架下Log4j2是如何工作的&#xff0c;你可能会担心&#xff0c;如果是使用Logback日志框架该怎么办呢&#xff1…

01_QT编译报错:Cannot find file:问题解决

QT编译报错&#xff1a;Cannot find file:问题解决 报错原因&#xff1a;创建路径存在中文字符&#xff0c;将文件路径改为英文字符即可