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

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: C语言中实现基于TCP协议的服务器客户通信,需要使用Socket编程。下面是一些基本步骤: 1. 建立服务器 首先需要建立服务器的socket。通过调用socket()函数创建服务器socket,指定socket的协议族(通常是AF_INET,即IPv4),socket类型(通常是SOCK_STREAM,即TCP流式套接字),和口号。之后调用bind()函数把服务器socket绑定到指定的地址和口。接下来就可以调用listen()函数开始监听客户连接请求。 2. 连接客户 客户需要调用socket()函数创建socket,同样指定协议族、socket类型和口号(这里可以随机指定一个未占用的口号)。之后调用connect()函数连接服务器的地址和口号即可。 3. 通信 一旦客户服务器建立连接,就可以通过读写socket进行通信服务器需要调用accept()函数接受客户连接请求,返回一个新的socket描述符用于和客户进行通信。之后可以使用send()函数向客户发送数据,使用recv()函数从客户接收数据。客户同样可以使用send()和recv()函数进行通信。 4. 结束连接 通信结束后,服务器客户需要分别调用close()函数关闭链接。 以上是基于TCP协议的服务器客户通信的基本步骤,具体实现过程需要详细的代码实现。在实际开发中,还需要注意处理错误和异常情况,以保证程序的稳定性和安全性。 ### 回答2: 基于TCP协议的服务器客户通信是一种常见的网络通讯方式。服务器网络上侦听特定口,接收客户的连接请求。当连接建立后,服务器客户之间可以进行数据传输。 实现基于TCP协议的服务器客户通信,需要遵循以下步骤: 1. 创建服务器的套接字(socket)并绑定IP地址和口号。 2. 监听客户的连接请求,等待客户连接。 3. 接受客户的连接请求,创建一个与客户通信的套接字。 4. 使用套接字进行数据传输,包括从客户接收请求和向客户发送响应。 5. 当通信完成后,关闭连接并释放资源。 对于客户,需要以下步骤: 1. 创建客户的套接字。 2. 连接服务器套接字。 3. 发送请求数据给服务器。 4. 接收服务器响应数据。 5. 关闭连接并释放资源。 在实现过程中,还需要注意以下方面: 1. 使用正确的IP地址和口号进行通信。 2. 服务器需要使用多线程或多进程进行并发处理,以支持多个客户同时连接。 3. 通信过程中需要加入一定的数据校验和错误处理机制,以提高通讯的可靠性。 总之,基于TCP协议的服务器客户通信是一种灵活、可靠的网络通讯方式,可以广泛应用于各种网络场景中,例如打印、文件传输、远程控制等。 ### 回答3: C 11是一种编程语言,可以用来实现基于TCP的服务器客户通信。TCP是传输控制协议的缩写,它提供了一种可靠的数据传输方式,被广泛用于互联网上的通信。 要实现基于TCP的服务器客户通信,需要用C 11语言编写两个程序:一个服务器程序和一个客户程序。服务器程序在运行时监听一个指定的口,等待客户程序的连接请求。当客户请求连接时,服务器程序接受连接请求,并创建一个新的进程或线程用于处理这个连接。 在服务器程序和客户程序之间进行数据传输时,需要使用TCP协议提供的套接字接口。服务器程序和客户程序都可以通过套接字接口创建一个套接字,用于进行数据传输。服务器程序可以使用accept函数来接受连接请求,而客户程序可以使用connect函数来连接服务器。 一旦连接建立,服务器程序和客户程序之间就可以通过套接字进行数据传输了。服务器程序可以使用send函数将数据发送给客户程序,而客户程序可以使用recv函数接收服务器发送的数据。数据传输结束后,服务器程序和客户程序都可以使用close函数关闭套接字。 总之,用C 11语言编写基于TCP的服务器客户通信程序需要了解TCP协议、套接字接口和相应的函数,熟练掌握C 11编程语言,并具有相应的开发经验和编程能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chfens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值