本文主要实现基于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