本文主要实现基于socket编程的聊天室,主要分为下面三个步骤:

(1)多用户聊天:一个服务器多个客户端,客户端信息显示在公共的服务端窗口,利用多线程实现;

         ——客户端双线程:一个接受线程一个发送线程(主线程);

         ——服务器单线程:接收线程;

(2)多用户广播界面:将信息显示到所有用户界面和服务器界面,同时服务器也能发言,利用多线程实现;

         ——客户端双线程:一个接受线程一个发送线程(主线程)

         ——服务器单线程:一个接收线程(主线程)一个发送线程;其中接受线程为每个连接开了单独的线程;

目录

一、基础流程

        服务器端

        客户端

二、多用户聊天

三、多用户广播聊天

客户端

服务端

效果展示

四、参考


一、基础流程

基于socket实现聊天室的流程如下:

对于socket编程,一般流程都为:搭建socket环境,创建套接字,进行连接后,开始通信。

服务器端:

(1)创建套接字:socket()函数;

(2)指定本机地址:bind()函数将本机地址和端口号与套接字连起来

(3)监听:listen()函数;监听连接请求,客户端发送连接请求;

(4)接受连接:accept()函数;

(5)发送接受消息:send()和recv()函数;

客户端:

(1)创建套接字:socket()函数;

(2)发送连接请求:connect()函数

         ——将套接字与主机地址和端口号连接起来:sockaddr_in addr;

         ——发送连接请求,等待服务器accept建立连接;

(3)发送接受消息:send()和recv()函数;

二、多用户聊天

首先,我们实现多个用户基于服务器聊天,所有信息都在服务器聊天框出现。

代码如下所示:

服务器端:

#include <stdio.h>
#include <stdlib.h>
#include "afxres.h"
#include <winsock2.h> // winsock2的头文件
#include <iostream>
#include <map>
#pragma comment(lib, "ws2_32.lib")
using namespace std;

map<SOCKET, string> client; // 存储socket和昵称对应关系

int main()
{

	system("chcp 65001"); // 设置中文
	// 加载winsock环境
	WSAData wd;
	if(WSAStartup(MAKEWORD(2,2), &wd) != 0){
		cout << "加载网络环境失败" << endl;
		return 0;
	}
	else
		cout << "加载网络环境成功" << endl;

	// 创建套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
	if(s == INVALID_SOCKET){
		cout << "创建套接字失败" << endl;
		WSACleanup();
	}
	else
		cout << "创建套接字成功" << endl;

	// 给套接字绑定ip地址和端口:bind函数
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int len = sizeof(sockaddr_in);
	if(bind(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR){
		cout << "服务器绑定端口和ip失败" <<endl;
		WSACleanup();
	}
	else
		cout << "server绑定端口和Ip成功" << endl;

	// 监听端口
	if(listen(s, 5) != 0){
		cout << "设置监听状态失败!" << endl;
		WSACleanup();
	}
	else
		cout << "设置监听状态成功!" << endl;

	cout<< "服务器监听连接中,请稍等......" << endl;

    // 循环接受:客户端发来的连接
	while(true){
		sockaddr_in addrClient;
		len = sizeof(sockaddr_in);
		SOCKET c = accept(s, (sockaddr*)&addrClient, &len);
		if( c == INVALID_SOCKET ){ // 一个失败我们就撤退,也可以去掉clean和return
			cout << "与客户端连接失败" << endl;
			WSACleanup();
			return 0;
		}
		
        //连接成功,开始发送消息
        char bufrecv[100] = {0}; //用来接受和发送数据
        int ret;
        ret = recv(c, bufrecv, 100, 0);
        client[c] = string(bufrecv);
        cout << "欢迎[" << client[c] << "]加入聊天室" << endl;
        string bufsend;
        bufsend = "欢迎[" + client[c] + "]加入聊天室";
        send(c, bufsend.data(), 100, 0);

        for(auto i : client){
            if(i.first ==  c)
                continue;
            send(i.first, bufsend.data(), 100, 0);
        }
        
        ret = 0;
        do{
            char buf[100] = {0};
            ret = recv(c, buf, 100, 0);
            cout << "[" << client[c] << "]: " << buf << endl;
        }while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error

        cout << "[" << client[c] << "]离开聊天室!" << endl;

	}
	
	// 关闭连接,释放资源
	closesocket(s);
	WSACleanup();
	return 0;
}

客户端:

#include <stdio.h>
#include <stdlib.h>
//#include "stdafx.h"
#include "afxres.h"
#include <winsock2.h> // winsock2的头文件
#include <iostream>
#include <map>
#pragma comment(lib, "ws2_32.lib")
using namespace std;

map<SOCKET, string> client;

int main()
{

	system("chcp 65001");
	// 加载winsock环境
	WSAData wd;
	if(WSAStartup(MAKEWORD(2,2), &wd) != 0){
		cout << "加载网络环境失败" << endl;
		return 0;
	}
	else
		cout << "加载网络环境成功" << endl;

	// 创建套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
	if(s == INVALID_SOCKET){
		cout << "创建套接字失败" << endl;
		WSACleanup();
	}
	else
		cout << "创建套接字成功" << endl;

	// 给套接字绑定ip地址和端口:bind函数
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int len = sizeof(sockaddr_in);
	if(connect(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR){
		cout << "客户端连接失败" <<endl;
		WSACleanup();
        return 0;
	}
	else
		cout << "客户端连接成功" << endl;

    // 发送和接受数据即可
    string name;
    char bufrecv[100] = {0}; 
    cout << "请输入你的昵称:";
    getline(cin, name); // 读入一整行,可以有空格
    send(s, name.data(), 100, 0);
	int ret;
	ret = recv(s, bufrecv, 100, 0); // 接受欢迎信息
	cout << bufrecv << endl;
    
    // while循环发送数据
	ret = 0;
	do{
        cout << "Enter the word: ";
		char bufrecv[100] = {0};
        cin.getline(bufrecv, 100); 
		ret = send(s, bufrecv, 100, 0);
	}while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error
    
	// 关闭连接,释放资源
	closesocket(s);
	WSACleanup();
	return 0;
}

 实现结果如上图所示,可以发现,成功建立连接,并且可以显示出信息。

但我们发现,当我们开启多个客户端,不能同时显示信息,只有关掉前面的客户端,后面的信息才能接着显示?

 经过分析我们发现,服务端每接受一个连接,就开始陷入该连接的while里面,不断接受该连接的信息,而没有跳出while,以得到其他的连接。

我们采用多线程来解决这个问题

对于每次建立的连接,我们将该连接开启一个线程用来处理服务器与该客户的信息接受,主线程一直处于监听和接受连接状态,从线程处于信息沟通状态。一旦建立一个连接,就给该连接开启一个线程用于发送和接受信息,从而实现同步。

实现的结果如下:

 此时,通过多线程我们成功实现了多用户同时通信。

修改代码如下:

服务端:

#include <stdio.h>
#include <stdlib.h>
//#include "stdafx.h"
#include "afxres.h"
#include <winsock2.h> // winsock2的头文件
#include <iostream>
#include <map>
#pragma comment(lib, "ws2_32.lib")
using namespace std;

map<SOCKET, string> client; // 存储socket和昵称对应关系
DWORD WINAPI Threadfun(LPVOID lpParameter);

int main()
{

	system("chcp 65001"); // 设置中文
	// 加载winsock环境
	WSAData wd;
	if(WSAStartup(MAKEWORD(2,2), &wd) != 0){
		cout << "加载网络环境失败" << endl;
		return 0;
	}
	else
		cout << "加载网络环境成功" << endl;

	// 创建套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
	if(s == INVALID_SOCKET){
		cout << "创建套接字失败" << endl;
		WSACleanup();
	}
	else
		cout << "创建套接字成功" << endl;

	// 给套接字绑定ip地址和端口:bind函数
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int len = sizeof(sockaddr_in);
	if(bind(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR){
		cout << "服务器绑定端口和ip失败" <<endl;
		WSACleanup();
	}
	else
		cout << "server绑定端口和Ip成功" << endl;

	// 监听端口
	if(listen(s, 5) != 0){
		cout << "设置监听状态失败!" << endl;
		WSACleanup();
	}
	else
		cout << "设置监听状态成功!" << endl;

	cout<< "服务器监听连接中,请稍等......" << endl;

	// 循环接受:客户端发来的连接
	while(true){
		sockaddr_in addrClient;
		len = sizeof(sockaddr_in);
		SOCKET c = accept(s, (sockaddr*)&addrClient, &len);
		if( c == INVALID_SOCKET ){ // 一个失败我们就撤退,也可以去掉clean和return
			cout << "与客户端连接失败" << endl;
			WSACleanup();
			return 0;
		}
		HANDLE hthread = CreateThread(NULL, 0, Threadfun, (LPVOID)c, 0, NULL);
		CloseHandle(hthread); // 关闭句柄,没用
	}
	
	// 关闭连接,释放资源
	closesocket(s);
	WSACleanup();

	return 0;
}

DWORD WINAPI Threadfun(LPVOID lpParameter){
	SOCKET c = (SOCKET)lpParameter;

	//连接成功,开始发送消息
    char bufrecv[100] = {0}; //用来接受和发送数据
    int ret;
    ret = recv(c, bufrecv, 100, 0);
    client[c] = string(bufrecv);
    cout << "欢迎[" << client[c] << "]加入聊天室" << endl;
    string bufsend;
    bufsend = "欢迎[" + client[c] + "]加入聊天室";
    send(c, bufsend.data(), 100, 0);
    
    ret = 0;
    do{
        char buf[100] = {0};
        ret = recv(c, buf, 100, 0);
        cout << "[" << client[c] << "]: " << buf << endl;
    }while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error

    cout << "[" << client[c] << "]离开聊天室!" << endl;
    return 0;
}

客户端代码不变。

三、多用户广播聊天

从上边我们可以发现,服务端只有接受信息功能,客户端只有发送信息功能,这就导致每个人只能通过服务端界面看消息,并且我们也不能实现一些小功能如:

(1)@某用户的消息提醒;

(2)用户退出提醒:某个用户退出,我们应该广播给每个用户界面,告诉该用户退出;

(3)服务器端作为管理员,也应该有说话的功能;

为了解决上面三个问题,我们进行如下探索。

客户端:

对于客户端,其建立连接后,一直处于while(true)的发送信息循环里,直到退出:

 那么我们可以考虑创建两个线程:接受信息线程和发送信息线程,从而使客户端既能接受信息,也能同步发送消息

我们将主线程用来发送数据(因为客户端主任务是发送),创建另一个线程用来接受数据:

 服务端:

服务端主线程是监听和接受连接,每接受一个连接就创建该连接的线程。那么为了能使服务端也作为发言方发送数据,我们创建副线程用来发送数据。同时,对于接受数据的线程,为了显示到别的用户端界面上,我们将收到的信息广播出去,让所有用户都能看见,同时广播实时用户状态,修改代码如下:

 创建的发送线程如下:

 那么至此,我们就修改好了两端的代码,进行尝试。

效果展示如下:

 可以看到,实现了客户端和用户端都是读写双线程,既能接受数据也能发送数据,同时如果用户离开也会广播信息。

修改后的代码如下:

服务端:

#include <stdio.h>
#include <stdlib.h>
//#include "stdafx.h"
#include "afxres.h"
#include <winsock2.h> // winsock2的头文件
#include <iostream>
#include <map>
#pragma comment(lib, "ws2_32.lib")
using namespace std;

map<SOCKET, string> client; // 存储socket和昵称对应关系
DWORD WINAPI Threadfun(LPVOID lpParameter);
DWORD WINAPI ThreadSend(LPVOID lpParameter);

int main()
{

	system("chcp 65001"); // 设置中文
	// 加载winsock环境
	WSAData wd;
	if(WSAStartup(MAKEWORD(2,2), &wd) != 0){
		cout << "加载网络环境失败" << endl;
		return 0;
	}
	else
		cout << "加载网络环境成功" << endl;

	// 创建套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
	if(s == INVALID_SOCKET){
		cout << "创建套接字失败" << endl;
		WSACleanup();
	}
	else
		cout << "创建套接字成功" << endl;

	// 给套接字绑定ip地址和端口:bind函数
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int len = sizeof(sockaddr_in);
	if(bind(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR){
		cout << "服务器绑定端口和ip失败" <<endl;
		WSACleanup();
	}
	else
		cout << "server绑定端口和Ip成功" << endl;

	// 监听端口
	if(listen(s, 5) != 0){
		cout << "设置监听状态失败!" << endl;
		WSACleanup();
	}
	else
		cout << "设置监听状态成功!" << endl;

	cout<< "服务器监听连接中,请稍等......" << endl;

    // 发送消息线程
    CloseHandle(CreateThread(NULL, 0, ThreadSend, (LPVOID)s, 0, NULL));
    
	// 循环接受:客户端发来的连接
	while(true){
		sockaddr_in addrClient;
		len = sizeof(sockaddr_in);
		SOCKET c = accept(s, (sockaddr*)&addrClient, &len);
		if( c == INVALID_SOCKET ){ // 一个失败我们就撤退,也可以去掉clean和return
			cout << "与客户端连接失败" << endl;
			WSACleanup();
			return 0;
		}

		HANDLE hthread = CreateThread(NULL, 0, Threadfun, (LPVOID)c, 0, NULL);
		CloseHandle(hthread); // 关闭句柄,没用
	}
	
	// 关闭连接,释放资源
	closesocket(s);
	WSACleanup();
	return 0;
}

DWORD WINAPI ThreadSend(LPVOID lpParameter){
	SOCKET c = (SOCKET)lpParameter;

    int ret = 0;
	do{
		char bufsend[100] = {0};
        cin.getline(bufsend, 100); 
        
        // 发送给所有用户端
        string str = "[Server]: " + string(bufsend);
        for(auto i : client)
		    ret = send(i.first, str.data(), 100, 0);
	}while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error

    return 0;
}
DWORD WINAPI Threadfun(LPVOID lpParameter){
	SOCKET c = (SOCKET)lpParameter;

	//连接成功,开始发送消息
    char bufrecv[100] = {0}; //用来接受和发送数据
    int ret;
    ret = recv(c, bufrecv, 100, 0);
    client[c] = string(bufrecv);

    string bufsend;
    bufsend = "欢迎[" + client[c] + "]加入聊天室";
    cout << bufsend << endl;

    for(auto i : client)
        send(i.first, bufsend.data(), 100, 0);
    
    ret = 0;
    do{
        char buf[100] = {0};
        ret = recv(c, buf, 100, 0);
        cout << "[" << client[c] << "]: " << buf << endl << endl;

        // 将接受到的信息广播
        string str1 = "[" + client[c] + "]: " + string(buf);
        for(auto i : client)
            send(i.first, str1.data(), 100, 0);
    }while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error

    string str2 = "[" + client[c] + "]离开聊天室!";
    cout << str2 << endl;
    for(auto i : client)
        send(i.first, str2.data(), 100, 0);
    
    return 0;
}

客户端:

#include <stdio.h>
#include <stdlib.h>
//#include "stdafx.h"
#include "afxres.h"
#include <winsock2.h> // winsock2的头文件
#include <iostream>
#include <map>
#pragma comment(lib, "ws2_32.lib")
using namespace std;

map<SOCKET, string> client;
DWORD WINAPI Threadfun(LPVOID lpParameter);

int main()
{
	system("chcp 65001");

	// 加载winsock环境
	WSAData wd;
	if(WSAStartup(MAKEWORD(2,2), &wd) != 0){
		cout << "加载网络环境失败" << endl;
		return 0;
	}
	else
		cout << "加载网络环境成功" << endl;

	// 创建套接字
	SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
	if(s == INVALID_SOCKET){
		cout << "创建套接字失败" << endl;
		WSACleanup();
	}
	else
		cout << "创建套接字成功" << endl;

	// 给套接字绑定ip地址和端口:bind函数
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int len = sizeof(sockaddr_in);
	if(connect(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR){
		cout << "客户端连接失败" <<endl;
		WSACleanup();
        return 0;
	}
	else
		cout << "客户端连接成功" << endl;

    // 发送和接受数据即可
    string name;
    char bufrecv[100] = {0}; 
    cout << "请输入你的昵称:";
    getline(cin, name); // 读入一整行,可以有空格
    send(s, name.data(), 100, 0);
	int ret;
    
    // 建立连接后,创建线程用于接受数据,主线程用来发送数据
    CloseHandle(CreateThread(NULL, 0, Threadfun, (LPVOID)s, 0, NULL));
    
    // while循环发送数据
	ret = 0;
	do{
		char bufrecv[100] = {0};
        cin.getline(bufrecv, 100); 
		ret = send(s, bufrecv, 100, 0);
	}while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error
    
	// 关闭连接,释放资源
	closesocket(s);
	WSACleanup();
	return 0;
}

DWORD WINAPI Threadfun(LPVOID lpParameter){
	SOCKET c = (SOCKET)lpParameter;
    int ret = 0;
    do{
        char buf[100] = {0};
        ret = recv(c, buf, 100, 0);
        cout << buf << endl << endl;
    }while(ret!=SOCKET_ERROR && ret!=0); //如果连接被关闭,返回0,否则返回socket_error

    return 0;
}

四、参考

优秀博文:

(465条消息) Socket 多人聊天室的实现 (含前后端源码讲解)(一)_socket的聊天程序代码及理解_宾有为的博客-CSDN博客

各个接口函数的解释:

socket技术详解(看清socket编程) - 枫飞飞 - 博客园 (cnblogs.com)

视频:

​​​​​​C/C++多线程实战教程:多线程客户端聊天室的实现!腾讯QQ的核心技术,老马就靠这个技术一战成名!_哔哩哔哩_bilibili

写代码部分(服务端)_哔哩哔哩_bilibili

C++windows网络编程学习笔记【简易聊天室】(二)_哔哩哔哩_bilibili

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐