上一章中,我们介绍了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_STREAM和SOCK_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号文件描述符)重定向到管道文件中,就能成功将读写分离。