在Linux下编写服务器端,用于接收客户端发送的消息,并转发给所有用户。
在Windows下编写客户端,用于向客户端发送消息,并接收服务器发送的其他用户的消息。
功能介绍
服务器在每个用户第一次发送消息后建立通信,用户可以开始接收和发送消息。以IP:端口号的形式标识每个用户的身份,用哈希表存储每个用户的身份信息和sockaddr结构体的映射。
可以自行设计注册和登录系统,分配用户名,将用户名代替IP+端口号标识以更好地辨认身份。
可以自行添加保存聊天记录到磁盘,用户登录后刷新最近的聊天记录到用户客户端。
效果展示
服务器端效果
用户端效果
Linux服务器端编写
Linux用到了socket编程接口如sendto,recvfrom,用到了网络序列字节流和本地序列字节流的转换,涉及大小端字节序的概念。且注意,Linux下的文字编码格式默认是UTF-8。
#include <iostream>
#include <cstring>
#include <string>
#include <unordered_map>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
首先包含必要的头文件。
int main(int argc, char *argv[])
{
std::string ip;
int port = 0;
if (argc != 2 && argc != 3)
{
std::cout << argv[0] << " 'Port' ('IP')" << std::endl;
return -1;
}
else
{
port = std::atoi(argv[1]);
if (argc == 3)
{
ip = argv[2];
}
}
UDPServer sev(ip, port);
sev.init();
sev.run();
return 0;
}
主函数里的内容。首先读取命令行参数,规定必须指定建立服务的端口,不必指定IP地址。
class UDPServer
{
private:
int _socketFd;
std::string _ip;
int _port;
char _inBuffer[1024];
user _userList;
}
类内成员定义,定义必须的文件描述符,IP地址和端口号,接收信息用到的缓存区,还有记录所有用户信息的userList。
class user
{
public:
std::unordered_map<std::string, struct sockaddr_in> userWebInfo;
void checkUser(std::string IP, unsigned int Port, struct sockaddr_in sockaddr)
{
std::string key = IP;
key += ":";
key += std::to_string(Port);
auto iter = userWebInfo.find(key);
if (iter == userWebInfo.end())
{
userWebInfo.insert({key, sockaddr});
}
}
};
如上是user类的定义,K值为用户的IP+端口号,标识每个唯一的用户,V值是用户客户端的sockaddr结构体,保存每个用户的属性,包含IP地址和端口号。
public:
UDPServer(std::string ip = "", int port = 0)
: _socketFd(-1), _ip(ip), _port(port)
{
}
~UDPServer()
{
}
构造函数和析构函数。
void init()
{
// 一、创建套接字
// AF_INET标识IPV4,SOCK_DGRAM表示面向数据报,用于UDP通信。
// 0表示阻塞式建立socket。
_socketFd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketFd < 0)
{
printf("服务器socket失败:%s : %d", strerror(errno), _socketFd);
exit(-1);
}
// 二、绑定网络信息,指明IP:Port
struct sockaddr_in local;
std::memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if (bind(_socketFd, (const struct sockaddr *)&local, sizeof(local)) == -1)
{
printf("服务器绑定失败:%s,[Fd:%d][%s:%d]", strerror(errno), _socketFd, _ip.c_str(), _port);
exit(-2);
}
printf("服务器绑定成功!Fd = %d", _socketFd);
}
初始化函数。
void run()
{
while (true)
{
printf("服务器#");
// recvfrom后两个参数是输出型参数,
// 返回发送消息过来的用户的属性
struct sockaddr_in client;
socklen_t clientLen = sizeof(client);
ssize_t recSz = recvfrom(_socketFd, _inBuffer, sizeof(_inBuffer), 0,
(struct sockaddr *)&client, &clientLen);
if (recSz < 0)
{
printf("服务器recvfrom失败:%s[fd = %d]", strerror(errno), _socketFd);
continue;
}
else
_inBuffer[recSz] = '\0';
std::string clientIP = inet_ntoa(client.sin_addr);
uint32_t clientPort = ntohs(client.sin_port);
_userList.checkUser(clientIP, clientPort, client);
broadCast(clientIP, clientPort, _inBuffer);
printf("%s:%d# %s\n", clientIP.c_str(), clientPort, _inBuffer);
}
}
服务器运行的函数。
void broadCast(std::string IP, int Port, std::string Info)
{
// Info是IP:Port用户发过来的消息
std::string message = "[";
message += IP;
message += ":";
message += std::to_string(Port);
message += "]# ";
message += Info;
for (auto &user : _userList.userWebInfo)
{
ssize_t sendSz = sendto(_socketFd, message.c_str(), message.size(), 0,
(struct sockaddr*)&(user.second), sizeof(user.second));
if(sendSz < 0)
{
printf("服务器broadCast失败:%s [sendSz = %ld]", strerror(errno), sendSz);
}
}
}
发送消息给所有用户的函数。
Windows客户端编写
Windows客户端同样使用socket编程接口如sendto,recvfrom等。也涉及网络字节序和本地字节序的转换,且Windows下的文字编码格式默认是GBK2312编码,因此无论发送消息还是接收消息,都需要转换格式。此外,客户端使用线程库,主线程用于发送消息,工作线程用于读取服务器广播的消息。
关于格式转换的代码来自c++ windows与linux通信中文乱码问题解决方法
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib") // 需要包含的链接库
#ifdef WIN32
#pragma execution_character_set("UTF-8")
#endif
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include <fstream>
#include <WinSock2.h> // windows socket 2.2版本
#include <windows.h>
#include <atlstr.h>
#include <atlbase.h>
#include <tchar.h>
首先建立好环境
WSADATA _wsaData; // 用于初始化
SOCKET _socketFd; // 套接字描述符
SOCKADDR_IN _server;// 服务端
定义全局变量,可以让线程直接访问,因为线程之间共享进程地址空间。
void rountine()
{
int _serverLen = sizeof(_server);
char _receiveBuffer[1024];
while (true)
{
long int recSZ = recvfrom(_socketFd, _receiveBuffer, sizeof(_receiveBuffer), 0,
(SOCKADDR*)&_server, &_serverLen);
if (recSZ > 0)
{
_receiveBuffer[recSZ] = '\0';
std::cout << std::endl << utfToGB2312(_receiveBuffer) << std::endl;
printf("%s", utfToGB2312("请输入#"));
}
}
}
工作线程的主要逻辑。
int main()
{
int ReceiverAddrSize = sizeof(SOCKADDR); // 服务端地址的大小
int Port = 自行填写开放的端口;
(void)WSAStartup(MAKEWORD(2, 2), &_wsaData);
_socketFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
_server.sin_family = AF_INET;
_server.sin_port = htons(Port);
_server.sin_addr.S_un.S_addr = inet_addr("自行填写你的主机IP");
printf("%s\n", utfToGB2312("你进入了许某的聊天室,开始聊天吧!如果输入后没有回应说明服务已关闭,请联系许某!"));
printf("%s\n", utfToGB2312("可输入中英文,按下回车以发送文本!欢迎反馈问题!"));
std::thread receiver(rountine);
receiver.detach();
while (true)
{
std::string buffer;
std::getline(std::cin, buffer);
long int sendSZ = sendto(_socketFd, gb2312ToUtf(buffer.c_str()), sizeof(buffer), 0,
(SOCKADDR*)&_server, sizeof(_server));
if (sendSZ < 0)
{
printf("send error:%s, [sendSZ = %d]", strerror(errno), sendSZ);
return -1;
}
Sleep(1);
}
closesocket(_socketFd); // 释放套接字
WSACleanup(); // 清空启动信息
return 0;
}
主函数的内容,sleep一秒的原因是客户端在发送信号比如ctrl+c终止进程时会发送一堆的换行符给服务器,具体原因没有定位到,但是加上sleep函数就可以避免。