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.rhkb.cn/news/307231.html

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

相关文章

vue3+ts中判断输入的值是不是经纬度格式

vue3ts中判断输入的值是不是经纬度格式 vue代码&#xff1a; <template #bdjhwz"{ record }"><a-row :gutter"8" v-show"!record.editable"><a-col :span"12"><a-input placeholder"经度" v-model:v…

如何进入Windows 11的安全模式?这里提供详细步骤

如果你在启动 Windows 11 电脑时遇到问题,重新启动到安全模式可能会有所帮助,该模式会暂时禁用驱动程序和功能以使你的电脑更稳定。这是如何做到的。 在启动时进入安全模式 在 Windows 7 及更早版本中,你通常可以在打开电脑后立即按功能键(如 F8)来启动安全模式。Micros…

u盘为什么一插上电脑就蓝屏,u盘一插电脑就蓝屏

u盘之前还好好的&#xff0c;可以传输文件&#xff0c;使用正常&#xff0c;但是最近使用时却出现问题了。只要将u盘一插入电脑&#xff0c;电脑就显示蓝屏。u盘为什么一插上电脑就蓝屏呢?一般&#xff0c;导致的原因有以下几种。一&#xff0c;主板的SATA或IDE控制器驱动损坏…

Java 实现自定义注解

一、interface 关键字 我们想定义一个自己的注解 需要使用 interface 关键字来定义。 如定义一个叫 MyAnnotation 的注解&#xff1a; public interface MyAnnotation { } 二、元注解 光加上 interface 关键字 还不够&#xff0c;我们还需要了解5大元注解 RetentionTargetDo…

JavaWeb--JavaScript-事件绑定/BOM/DOM编程

目录 1. 事件绑定 1.1. 什么是事件 1.2. 常见事件 1.3. 事件的绑定 1.3.1. 属性绑定 1.3.2. DOM编程绑定 1.4. 事件的触发 1.4.1. 行为触发 1.4.2. DOM编程触发 2. BOM 编程 2.1. 什么是 BOM 2.2. window对象的常见属性(了解) 2.3. window对象的常见方法(了解) 2…

YOLOv8绝缘子边缘破损检测系统(可以从图片、视频和摄像头三种方式检测)

可检测图片和视频当中出现的绝缘子和绝缘子边缘是否出现破损&#xff0c;以及自动开启摄像头&#xff0c;进行绝缘子检测。基于最新的YOLO-v8训练的绝缘子检测模型和完整的python代码以及绝缘子的训练数据&#xff0c;下载后即可运行。&#xff08;效果视频&#xff1a;YOLOv8绝…

softmax回归:多分类问题的解码器

随着人工智能技术的不断发展&#xff0c;分类问题在机器学习领域中的地位日益凸显。在众多分类算法中&#xff0c;softmax回归以其独特的优势和广泛的应用场景&#xff0c;成为了处理多分类问题的有力工具。本文将深入探讨softmax回归的原理、应用及其优缺点&#xff0c;以期为…

使用uniapp实现小程序获取wifi并连接

Wi-Fi功能模块 App平台由 uni ext api 实现&#xff0c;需下载插件&#xff1a;uni-WiFi 链接&#xff1a;https://ext.dcloud.net.cn/plugin?id10337 uni ext api 需 HBuilderX 3.6.8 iOS平台获取Wi-Fi信息需要开启“Access WiFi information”能力登录苹果开发者网站&…

Word wrap在计算机代表的含义(自动换行)

“Word wrap”是一个计算机术语&#xff0c;用于描述文本处理器在内容超过容器边界时自动将超出部分转移到下一行的功能。在多种编程语言和文本编辑工具中&#xff0c;都有实现这一功能的函数或选项。 在编程中&#xff0c;例如某些编程语言中的wordwrap函数&#xff0c;能够按…

数仓维度建模

维度建模 数仓建模方法1. 范式建模法&#xff08;Third Normal Form&#xff0c;3NF&#xff09;2. 维度建模法&#xff08;Dimensional Modeling&#xff09;3. 实体建模法&#xff08;Entity Modeling&#xff09; 维度建模1. 事实表事实表种类事务事实表周期快照事实表累计快…

洛谷-P1596 [USACO10OCT] Lake Counting S

P1596 [USACO10OCT] Lake Counting S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) #include<bits/stdc.h> using namespace std; const int N110; int m,n; char g[N][N]; bool st[N][N]; //走/没走 int dx[] {-1,-1,-1,0,0,1,1,1}; //八联通 int dy[] {-1,0,1,1,-1,1…

Matlab调C/C++简单模板例子

如果你是需要快速搭建一个matlab调c/c环境&#xff0c;这篇文章可以参考 有了c代码&#xff0c;想在matlab里面调用&#xff0c;可以参考我这个模板 matlab调用代码&#xff1a; clear all close all clcinput1 1; input2 2;[output1,output2] mexfunction(input1,input2);…

GitHub repository - Branch - SSH clone URL - Clone in Desktop - Download ZIP

GitHub repository - Branch - SSH clone URL - Clone in Desktop - Download ZIP 1. Branch2. SSH clone URL3. Clone in Desktop4. Download ZIPReferences 1. Branch 显示当前分支的名称。从这里可以切换仓库内分支&#xff0c;查看其他分支的文件。 2. SSH clone U…

Vue.js组件精讲 第2章 基础:Vue.js组件的三个API:prop、event、slot

如果您已经对 Vue.js 组件的基础用法了如指掌&#xff0c;可以跳过本小节&#xff0c;不过当做复习稍读一下也无妨。 组件的构成 一个再复杂的组件&#xff0c;都是由三部分组成的&#xff1a;prop、event、slot&#xff0c;它们构成了 Vue.js 组件的 API。如果你开发的是一个…

52.网络游戏逆向分析与漏洞攻防-基础数据分析筛选-面对庞大的数据如何找到节奏

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 内容参考于&#xff1a;如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&…

.NET SignalR Redis实时Web应用

环境 Win10 VS2022 .NET8 Docker Redis 前言 什么是 SignalR&#xff1f; ASP.NET Core SignalR 是一个开放源代码库&#xff0c;可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。 适合 SignalR 的候选项&#xff1a; 需要从服…

二、Maven安装

Maven安装 一、Centos7.9安装1.下载2.安装3.设置国内镜像4.设置maven安装路径 一、Centos7.9安装 1.下载 第一种&#xff1a;官网下载最新版本&#xff1a;http://maven.apache.org/download.cgi第二种&#xff1a;其他版本下载&#xff1a;https://archive.apache.org/dist/…

K-means和逻辑回归

逻辑回归 一个事件的几率是该事件发生的概率/该事件不发生的概率&#xff1a;P/&#xff08;1-P&#xff09; 对数几率是&#xff1a;log(P/&#xff08;1-P&#xff09;) **考虑对输入x分类的模型&#xff1a;**log(P/&#xff08;1-P&#xff09;)wx 则 Pexp(wx)/(exp(w*x)…

程序员Java.vue,python前端后端爬虫开发资源分享

bat面试资料 bat面试题汇总 提取码&#xff1a;724z 更多资料

【迅为iMX6Q】开发板 Linux version 6.6.3 SD卡 启动

开发环境 win10 64位 VMware Workstation Pro 16 ubuntu 20.04 【迅为imx6q】开发板&#xff0c; 2G DDR RAM linux-imx 下载 使用 NXP 官方提供的 linux-imx&#xff0c;代码地址为&#xff1a; https://github.com/nxp-imx/linux-imx 使用 git 下载 linux-imx&#xff…