怎么创建视频网站,wordpress模板排行榜,中卫网站设计公司招聘,做网站需要学啥Linux网络#xff1a;UDP socket - 简单聊天室 聊天通信架构ServerInetAddrUdpServerMessageRoutermain Client测试 聊天通信架构
本博客基于Linux实现一个简单的聊天通信服务#xff0c;以熟悉Linux的网络接口。
总代码地址#xff1a;[UDPsocket-简单聊天通信]
文件结构… Linux网络UDP socket - 简单聊天室 聊天通信架构ServerInetAddrUdpServerMessageRoutermain Client测试 聊天通信架构
本博客基于Linux实现一个简单的聊天通信服务以熟悉Linux的网络接口。
总代码地址[UDPsocket-简单聊天通信]
文件结构如下 在server文件夹中包含三个类分别写在三个文件中
InetAddr.hpp记录通信主机的ip和port方便进行通信UdpServer.hpp完成服务端UDP套接字的创建并接收来自客户端的消息MessageRouter对收到的消息进行业务处理
两个文件中的main.cpp是源文件分别编译得到服务端与客户端的可执行文件。 Server
InetAddr
class InetAddr
{
private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};类成员
_addr套接字地址_ip主机地址_port主机端口号
其实在_addr内部已经存储了地址与端口号这一层封装的意义是提供更加便捷的接口来访问地址与端口。
构造函数
InetAddr(const struct sockaddr_in addr): _addr(addr)
{_ip inet_ntoa(addr.sin_addr);_port ntohs(addr.sin_port);
}构造函数接受一个套接字地址addr随后初始化_ip与_port。
对_ip地址来说要把四字节的序列通过inet_ntoa转化为字符串形式对_port端口来说则是要把网络字节序转化为主机字节序
基本get接口
std::string ip()
{return _ip;
}uint16_t port()
{return _port;
}struct sockaddr_in addr()
{return _addr;
}这些接口用于外部访问类成员。
操作符重载operator
bool operator(const InetAddr other) const
{return _ip other._ip _port other._port;
}后续要完成客户的网络地址之间的身份标识通过_ip _port的组合来确定一个客户这样就可以区分前后是否是同一个人发消息。因此此处要重载operator辨别两个UDP报文是否是同一个客户发送的。 UdpServer
类架构
using func_t std::functionvoid(int sockfd, std::string message, InetAddr cliAddr);class UdpServer
{
private:int _sockfd;uint16_t _port;func_t _callback;
};类成员
_sockfd创建网络套接字得到的文件描述符后续通过读写该描述符操作网络_port指定服务端监听的端口_callback一个回调函数当服务端收到消息后调用该回调函数处理信息这一层操作的意义在于把UDP套接字与业务逻辑进行解耦
枚举错误码
enum
{SOCKET_ERROR 1, // 套接字错误BIND_ERROR, // 绑定错误
};为例方便后续指明错误类型此处枚举了三个错误码。
构造
UdpServer(uint16_t port, func_t callback): _port(port), _callback(callback)
{_sockfd socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字if (_sockfd 0)exit(SOCKET_ERROR);struct sockaddr_in addr; // 初始化套接字信息bzero(addr, sizeof(addr)); addr.sin_family AF_INET;addr.sin_port htons(_port);addr.sin_addr.s_addr INADDR_ANY;// 绑定套接字int n bind(_sockfd, (struct sockaddr*)addr, sizeof(addr)); if (n 0)exit(BIND_ERROR);
}在构造函数中实现UDP的套接字创建。
参数
port该服务开放的端口callback上层处理消息的业务逻辑的回调函数
随后通过socket函数创建套接字参数
AF_INET使用ipv4通信SOCK_DGRAM使用UDP进行通信0不用管直接填0即可
得到一个文件描述符_sockfd后续通过操作该文件描述符进行网络通信。
但是当前套接字还只是一个内存中的变量操作系统还没有进行真正的网络监听此时要将套接字绑定起来。
首先初始化套接字地址的信息
bzero(addr, sizeof(addr)); // 清空内存原有内容
addr.sin_family AF_INET; // 使用ipv4通信
addr.sin_port htons(_port); // 使用指定端口号
addr.sin_addr.s_addr INADDR_ANY; // 绑定地址此处addr.sin_addr.s_addr表示该套接字接收来自于哪些地址的请求比如填入127.0.0.1那么就只有127.0.0.1地址可以与该服务通信。而填入0.0.0.0表示可以接收任意地址的请求此处INADDR_ANY就代表0.0.0.0只不过被封装为了一个宏。
最后通过bind进行绑定此时就创建了一个UDP套接字基于ipv4进行通信监听任意地址发送的请求。
开始服务
void start()
{while (true){char buffer[1024];struct sockaddr_in cliAddr;bzero(cliAddr, sizeof(cliAddr));socklen_t len sizeof(cliAddr);int n recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)cliAddr, len);if (n 0){buffer[n] \0;_callback(_sockfd, buffer, cliAddr);}}
}最后写一个start函数这个函数用于接收来自客户端的消息。通过recvfrom接口读取网络中的UDP报文读取到buffer数组中为了防止字符串没有结尾最后buffer[n] \0添加一个字符串的终止符。因为recvfrom返回接收到的字符个数所以最后一个字符的下标为n - 1在下标n处补充一个\0。
接收到消息后通过_callback把套接字文件描述符_sockfd接收到的数据buffer以及客户端信息cliAddr发送给业务层处理。
此处注意
using func_t std::functionvoid(int sockfd, std::string message, InetAddr cliAddr);这是_callback函数的类型其中std::string message是一个普通的std::string类型他不是引用也不是指向字符串的指针。因为buffer是个在栈区的数组等到下一轮while循环这个数组的内容就是未定义的。所以会导致指针越界访问到错误数据等问题。因此不能使用指针或引用而是让std::string对buffer内的数据进行一次拷贝。cliAddr同理不是一个引用或者指针要进行一次拷贝。 MessageRouter
MessageRouter是业务层的逻辑接收到一条消息后处理消息并发送回给客户端。 如图当UdpServer接收到来自客户端的消息后MessageRouter要把这个消息转发给其他客户端也就是说MessageRouter的任务就是转发消息。而发送消息需要通过套接字文件描述符这也就是为什么在刚才的_callback要传一个_sockfd。
类架构
class MessageRouter
{
private:std::vectorInetAddr _online_user;pthread_mutex_t _mutex PTHREAD_MUTEX_INITIALIZER;
};MessageRouter要维护所有的用户所以使用一个数组来存储所有的客户端。每个客户端用一个InetAddr表示也就是一个ip port确定一个唯一的客户端。
因为后续要引入多线程会出现并发访问数组的问题此处使用一把_mutex锁来进行并发控制。
增加用户
bool addUser(const InetAddr user)
{pthread_mutex_lock(_mutex);for (auto o_user : _online_user) // 遍历数组{if (user o_user) // 用户已存在{pthread_mutex_unlock(_mutex);return false;}}_online_user.push_back(user); // 新增用户pthread_mutex_unlock(_mutex);return true;
}增加一个用户就要访问数组_online_user访问之前要加锁访问结束后再解锁。
访问前先遍历数组查看是否当前用户已经存在如果存在直接返回返回前别忘了解锁。如果不存在则尾插新用户到数组中。
删除用户
bool delUser(const InetAddr user)
{pthread_mutex_lock(_mutex);auto it find(_online_user.begin(), _online_user.end(), user);if (it _online_user.end()){pthread_mutex_unlock(_mutex);return false;}_online_user.erase(it);pthread_mutex_unlock(_mutex);return true;
}删除用户与添加用户同理先遍历数组如果用户不存在直接返回。如果存在那么删掉该用户。
消息转发
struct SendPackage
{SendPackage(MessageRouter* self, int sockfd, std::string message, InetAddr cliAddr): _self(self), _sockfd(sockfd), _message(message), _cliAddr(cliAddr){}MessageRouter* _self;int _sockfd;std::string _message;InetAddr _cliAddr;
};为了不影响主线程接收消息提高进行处理消息的效率此处将消息转发的任务交给一个线程来完成。而Linux中线程要使用一个void*(void*)类型的函数这样就不好将参数传递给线程了所以要先用一个结构体将所有参数进行打包。再把指向该结构体的指针转为void*传给线程。
SendPackage是一个内部类用于对线程所需的参数进行打包让线程可以进行消息的转发。
_self指向MessageRouter的指针因为线程要访问所有用户也就是访问_online_user所以要一个指针回指来访问_sockfd套接字文件描述符进行消息转发也就是进行网络通信网络通信依赖于套接字文件描述符_message要转发的消息_cliAddr发送方客户端的信息
static void* messageSender(void* args)
{SendPackage* sendpkg (SendPackage*)args;std::string msg [ sendpkg-_cliAddr.ip() : std::to_string(sendpkg-_cliAddr.port()) ] sendpkg-_message;std::cout sending... msg std::endl;pthread_mutex_lock(sendpkg-_self-_mutex);for (auto usr : sendpkg-_self-_online_user){struct sockaddr_in cliaddr usr.addr();sendto(sendpkg-_sockfd, msg.c_str(), msg.size(), 0, (sockaddr*)cliaddr, sizeof(cliaddr));}pthread_mutex_unlock(sendpkg-_self-_mutex);delete sendpkg;return nullptr;
}该函数是线程执行的函数用于对消息进行转发首先将参数void*转回SendPackage*也就是刚刚的参数包结构体。
随后拼接字符串msg这是要转发消息内容。格式为
[ip:port] 消息前面的[]表明这是哪一个用户发送的消息后面是具体的消息内容。
随后服务端输出一条日志std::cout sending... msg std::endl;表示自己转发了这条消息。
随后访问_online_user数组遍历所有成员并且对通过sendto函数进行消息转发。
转发完消息后进行解锁并且delete释放sendpkg这是一个堆区上的对象后续会讲解原因。
回调主逻辑
void router(int sockfd, std::string message, InetAddr cliAddr)
{// 首次发消息 - 注册addUser(cliAddr);// 用户退出if (message /quit){delUser(cliAddr);return;}// 线程转发消息SendPackage* sendpkg new SendPackage(this, sockfd, message, cliAddr);pthread_t tid;pthread_create(tid, 0, messageSender, messageSender);pthread_detach(tid);
}这个函数就是UdpSerever中回调的函数当用户要发消息时首先添加该用户addUser(cliAddr)如果用户已经存在addUser函数内部也不会重复添加。
如果用户想要退出输入/quit此时会进行删除delUser。
如果前面已经添加好了用户随后就开始进行消息转发此时对参数进行打包new SendPackage。这里要用new创建把这个参数包创建在堆区因为router创建完线程就直接退出了此时栈区中的所有数据都会销毁。那么线程就无法读取到栈区中的参数包所以要把参数包创建在堆区随后让线程自己释放。
创建线程时给线程指定函数messageSender参数messageSender这样线程就会去调用函数然后完成数据的转发。
最后router退出之前先把创建的线程detach让其自己回收。 main
在main.cpp中完成所有逻辑的拼接启动整个服务。
void useage(char* argv[])
{std::cout useage: std::endl;std::cout \t argv[0] port std::endl;
}int main(int argc, char* argv[], char* env[])
{if (argc ! 2){useage(argv);return 0;}// 主逻辑return 0;
} 首先判断用户执行该程序的指令用户需要指定一个端口表示该服务使用的端口。如果没有指定则useage输出提示用户输入一个端口号。
主逻辑
uint16_t port std::stoi(argv[1]);MessageRouter msgRouter;
auto func std::bind(MessageRouter::router, msgRouter, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);UdpServer udpSvr(port, func);
udpSvr.start();当确定用户输入了一个端口后首先用port接收这个端口从字符串转为数字。
随后把MessageRouter的router函数作为回调函数传给udpSvr对象。由于该函数是一个类内的函数第一个参数为this指针。因此使用bind把第一个参数绑定为msgRouter也就是一个具体对象的指针。这样新函数func的类型就与UdpServer的回调函数一致。
最后传入端口号port与回调函数func启动服务。 Client
客户端的任务很简单只需要完成数据的发送与接收即可。此处把发送消息和接收消息交给两个不同的线程去完成。
线程参数
struct SockInfo
{SockInfo(int sockfd, const struct sockaddr_in sockaddr): _sockfd(sockfd), _sockaddr(sockaddr){}int _sockfd;struct sockaddr_in _sockaddr;
};由于要使用多线程和之前也一样要把所有参数放到一个结构体一起传参。此处只需要把服务端的信息以及通信的套接字文件描述符传送给线程。
_sockfd与服务端通信的文件描述符_sockaddr服务端的套接字地址信息
接收消息
void* recvMessage(void* args)
{SockInfo* sockInfo (SockInfo*)args;while(true){char buffer[1024];int n recvfrom(sockInfo-_sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);if (n 0){buffer[n] \0;std::cout buffer std::endl;}}return nullptr;
}首先把收到的void*参数包转回SockInfo*。
随后进入死循环接收来自服务端的消息。此处recvfrom的最后两个参数设为nullptr表示不关心谁发送的消息忽略消息发送方的地址与端口信息。因为通过sockInfo-_sockfd通信而这个套接字就是在和服务端通信无需再确认身份了。
收到消息后直接cout buffer endl输出接收到的消息。
发送消息
void* sendMessage(void* args)
{SockInfo* sockInfo (SockInfo*)args;std::string message;while(true){std::getline(std::cin, message);sendto(sockInfo-_sockfd, message.c_str(), message.size(), 0, (sockaddr*)sockInfo-_sockaddr, sizeof(sockInfo-_sockaddr));}return nullptr;
}同理解析出参数包后进入一个死循环。每轮循环等待用户输入一个消息随后把这个消息发送给服务端服务端会进行消息转发。
主函数
void usage(char* argv[])
{std::cout Usage:\n\t;std::cout argv[0] server_ip server_port std::endl;
}int main(int argc, char* argv[], char* env[])
{if (argc ! 3){usage(argv);return 0;}// 主逻辑return 0;
}主函数中需要用户输入一个ip和一个port表示客户端的地址和端口如果输入错误调用usage提示用户。
主逻辑
// 解析地址与端口
std::string ip argv[1];
uint16_t port std::stoi(argv[2]);// 创建套接字
int sockfd socket(AF_INET, SOCK_DGRAM, 0);// 初始化服务端信息
struct sockaddr_in server;
bzero(server, sizeof(server));
server.sin_family AF_INET;
server.sin_addr.s_addr inet_addr(ip.c_str());
server.sin_port htons(port);// 创建线程
SockInfo sockInfo(sockfd, server);
pthread_t sender;
pthread_t recver;
pthread_create(sender, 0, sendMessage, sockInfo);
pthread_create(recver, 0, recvMessage, sockInfo);pthread_join(sender, nullptr);
pthread_join(recver, nullptr);首先创建套接字然后初始化服务端的信息。在sockaddr_in server中就填充了服务端的地址与端口号。
客户端无需进行bind绑定在第一次通过sendto发送消息时操作系统会自动为其分配一个端口号。
随后创建两个线程分别执行sendMessage和recvMessage进行消息的接收与发送。
此处有一个小细节sockInfo不是new出来的而是直接存储在栈区的变量。这个情况与之前有所不同之前是因为router函数创建完线程后就直接退出了。而此处的主函数不能退出主函数退出整个进程都终止了所以栈区中的数据会一直存在不需要new。
最后在main函数中通过join等待两个线程。 测试 左上角是服务端剩余三个终端是客户端。首先右上角的终端启动发送了一个hello随后服务端把hello返送回给了右上角的终端。因为之前写逻辑时只有客户端发送一次消息服务端才会把客户端加入到_online_users中下面两个终端没有发消息所以服务端不知道这两个客户端存在也就没有转发消息。 随后左下角终端发送了iammike这个消息被转发给了右上角的终端因为右上角的终端已经在_online_users中。 发送一段时间消息后右上角终端输入/quit此时服务端将其删除。最后左下角终端发送iamlisa此时右上角终端收不到该消息了说明右上角终端已经成功退出。