C++——网络聊天室,UDP实现Linux服务器和Windows客户端通信

8 篇文章 0 订阅

在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函数就可以避免。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值