Windows平台基于TCP协议Sockets多人聊天室控制台程序

本文介绍了一个基于socket通信的多客户端群聊系统的实现细节,包括服务端与客户端的代码设计,采用多线程处理多客户端同时通信,实现消息的接收与转发。文章探讨了连接建立、数据传输和连接释放等阶段的关键操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

程序实现了基本的多客户端“群聊”功能,还有一些问题需要完善。
通过socket实现服务端与客户端间通信,服务端采用多线程实现与多个客户端同时通信,接受客户端的消息并转发至所有客户端。客户端采用多线程同时接受与发送消息。

服务端:

      #include<iostream>
        #include"stdlib.h"
        #include<winsock2.h>
        #include<string>
        #include<stdio.h>
        //#pragma comment(lib,"ws2_32.lib")**项目-属性-链接库-输入-附加依赖项加载ws2_32.lib(取消勾选“从父级或项目默认设置继承”)**
        using namespace std;
        
        int count = 0;
        SOCKET clientSocket[1024] = { 0 };
    DWORD WINAPI sendThread(LPVOID lpParam)
    {
    	char str[1024] = { 0 };
    	char recBuf[1024] = {0};
    	char sendBuf[1024] = {0};
    	int i = (int)lpParam;
    	while (1)
    	{
    		int receByte = recv(clientSocket[i-1], recBuf, sizeof(recBuf), 0);
    		if (receByte == -1)
    		{
    			cout << "接收来自客户端 "<<i<<" 的消息失败!" << endl;
    			break;
    		}
    		else
    		{
    			cout << "接收到来自客户端 " << i << " 的消息:" << recBuf << endl;
    			for (int j = 0; j < ::count; j++)
    			{
    				int sendByte = send(clientSocket[j], recBuf, sizeof(recBuf), 0);//返回发送数据的总和
    				if (sendByte < 0)
    				{
    					cout << "发送失败" << endl;
    				}
    				else
    				{
    					cout << "成功发送到客户端  " << j+1 << "  的消息:";
    					cout << recBuf << endl;
    					continue;
    				}
    			}
    		}
    		
    	}
    
    	closesocket(*clientSocket);
    	return 0;
    }

    int main(void)
    {
    	WSADATA wsaData;
    	WORD require;
    	require = MAKEWORD(2, 2);
    	if (WSAStartup(require, &wsaData)!=0)//成功加载返回0;
    	{
    		cout << "加载winsock失败!" << endl;
    		return -1;
    	}
    	if (LOBYTE(wsaData.wVersion)!=2||HIBYTE(wsaData.wVersion)!=2)
    	{
    		cout << "请求版本失败!" << endl;
    		return -1;
    	}
    	cout << "请求版本成功!" << endl;
    	SOCKET severSocket;
    	severSocket=socket(AF_INET, SOCK_STREAM, 0);
    	if (severSocket == -1)
    	{
    		cout << "创建socket失败!" << endl;
    		return -1;
    	}
    	cout << "创建socket成功!" << endl;
    	sockaddr_in addr;
    	addr.sin_family = AF_INET;
    	addr.sin_addr.S_un.S_addr = INADDR_ANY;
    	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    	addr.sin_port = htons(25000);
    	int b;
    	b=bind(severSocket, (sockaddr*)&addr, sizeof(addr));
    	if (b == -1)
    	{
    		cout << "绑定socket失败!" << endl;
    		return -1;
    	}
    	cout << "绑定socket成功!" << endl;
    	b=listen(severSocket, 10);
    	if (b == -1)
    	{
    		cout << "listen失败!" << endl;
    		return -1;
    	}
    	cout << "listen成功......" << endl;
    
    	//主线程用于接收请求
    
    	int i = 0;
    	
    	while (1)
    	{
    		//clientSocket = (SOCKET*)malloc(sizeof(SOCKET));
    		clientSocket[i] = accept(severSocket, NULL, NULL);
    		i++;
    		::count++;
    		cout << "客户端" << i << "已连接到服务器" << endl;
    		CreateThread(NULL, 0, &sendThread, (LPVOID*)i, 0, NULL);
    	}
    	
    	closesocket(severSocket);
    	WSACleanup();

    	system("pause");
    	return 0;
    }

客户端:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
using namespace std;

DWORD WINAPI receThread(LPVOID lpParam)
{	
	char receBuff[1024] = { 0 };
	SOCKET *serveSocket = (SOCKET*)lpParam;
	while (1)
	{
		int recvByte = recv(*serveSocket, receBuff, sizeof(receBuff), 0);
		if (recvByte >0)
		{
			cout << "接收到来自服务器的消息:" << receBuff << endl;
			continue;
		}
		else
		{
			cout << "收信结束" << endl;
			break;
		}
	}
	closesocket(*serveSocket);
	return 0;
}  
int main(void)
{
	WSADATA wsaData;
	WORD require = MAKEWORD(2, 2);
	if (WSAStartup(require, &wsaData) != 0)
	{
		cout << "加载winsock失败" << endl;
		return -1;
	}
	cout << "加载winsock成功" << endl;
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		cout << "请求版本失败" << endl;
		return -1;
	}
	cout << "加载版本成功" << endl;

	SOCKET clientSocket;
	clientSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (clientSocket == -1)
	{
		cout << "创建socket失败" << endl;
		return -1;
	}
	cout << "创建socket成功" << endl;
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//10.230.136.55 
	addr.sin_port = htons(25000);
	int b;
	b = connect(clientSocket, (sockaddr*)&addr, sizeof(addr));//////
	if (b == -1)
	{
		cout << WSAGetLastError() << endl;
		cout << "连接失败" << endl;
		return -1;
	}
	cout << "已成功连接到服务器,可以发送消息" << endl;


	char str[1024];
	char sendBuff[1024] = { 0 };
	char receBuff[1024] = { 0 };
	CreateThread(NULL, 0, &receThread, &clientSocket, 0, NULL);
	while (1)
	{
		cout << "请输入消息:" << endl;
		gets_s(str);
		strcpy(sendBuff, str);
		int sendByte;
		sendByte = send(clientSocket, sendBuff, sizeof(sendBuff), 0);
		if (sendByte < 0)
		{
			cout << "发送失败" << endl;
			break;
		}
		else
		{
			cout << "发送成功" << endl;
			continue;
		}
	}
	closesocket(clientSocket);
	WSACleanup();
	system("pause");
	return 0;
}

客户端服务端均需在项目-属性中关闭SDL检查

遇到的部分问题:

  1. connect连接不上,错误WSAGetLastError() 10049:表示IP或者端口不能用,逐步调试确定IP是否有误及端口号是否被设为0;
  2. socket返回的值是一个文件描述符,socket类型是定义为int型的,错误是返回-1,INVALID_SOCKET 就是被定义为 -1
  3. WSAStartup()中第一个参数不严格,如果传入的版本不存在会自动调用最低版本。
  4. 协议族:PF_INET(表示使用Internet的TCP/IP协议族)
    服务:SOCK_STREAM(表示使用流式服务,也就是TCP服务)
  5. LOWORD()得到一个32bit数的低16bit
    HIWORD()得到一个32bit数的高16bit
    LOBYTE()得到一个16bit数的低字节
    HIBYTE()得到一个16bit数的高字节
  6. 其余函数及函数参数问题就百科吧

理论:
运输层主要有两种协议,传输控制协议TCP(transmission control protocol)、用户数据报协议UDP(user datagram protocol)
网络层使用IP协议(Internet ptotocol)网际协议,网络层另一个任务选择合适的路由,TCP连接的端点不是主机,不是主机的IP地址,不是应用进程,也不是运输层地协议端口,TCP连接的端点是socket(套接字);端口号拼接到IP地址即构成了套接字(192.3.4.5:80)每一条TCP连接唯一地被通信两端的两个端点(即两套接字)所确定:
TCP连接::={socket1,socket2}={(IP1:port1 ),(IP2:port2)}
socket=(IP地址:端口号)

1.连接建立阶段
服务端情况:

  1. bind
    套接字被建立后,端口号和IP地址都是空的,应用进程要调用bind来指明套接字的本地地址(本地端口号和本地IP地址),在服务端就是把本地地址绑定到套接字,在客户端可以不调用bind,由操作系统内核自动分配一格动态端口号;
  2. lishten
    服务器调用bind后,必须调用收听listen把套接字设置为被动方式,以便随时接受客户的服务请求(UDP服务器由于只提供无连接服务,不使用listen系统调用。)
  3. accept
    服务器紧接着调用accept,以便把远地客户进程发来的连接请求提取出来,系统调用accept就是要指明从哪一个套接字发起的连接,服务器必须能够同时处理多个连接,就是并发方式工作的服务器,主服务器进程一调用accept,就为每一个新的连接请求创建一个新的套接字,并把这个套接字的标识符返回给发起连接的客户。主服务器进程还要创建一个从属服务器进程来处理新的链接,从属服务器用套接字与客户链接,主服务器进程用原来的套接字从新调用accept接收下一个连接请求

客户端情况:
客户已经调用socket创建了套接字,客户调用connect,以便和远地服务器建立连接,(相当于客户发出的连接请求,在connect中,客户必须指明远地端点)(端口号及IP地址)。

2,数据传送阶段
客户和服务器都在TCP连接上使用send系统调用传送数据,使用recv系统调用接口数据。通常客户使用send发送请求,而服务器使用send发送回答,服务器使用recv接收客户用send调用发送的请求。客户在发完请求后用revc接收回答。

调用send需要三个变量:

  1. 数据要发送的套接字的描述符
  2. 要发送的数据的地址
  3. 数据的长度

调用recv需要三个变量:

  1. 套接字的描述符
  2. 缓存的地址
  3. 缓存空间的长度

3,连接释放阶段
调用close释放连接,撤销套接字。

一、实验目的 1.掌握通信规范的制定及实现。 2.练习较复杂的网络编程,能够把协议设计思想应用到现实应用中。 二、实验内容和要求 1.进一步熟悉VC++6编程环境; 2.利用VC++6进行较复杂的网络编程,完成网络聊天室的设计及编写; 三、实验(设计)仪器设备和材料 1.计算机及操作系统:PC机,Windows; 2.网络环境:可以访问互联网; 四、 TCP/IP程序设计基础 基于TCP/IP的通信基本上都是利用SOCKET套接字进行数据通讯,程序一般分为服务器端和用户端两部分。设计思路(VC6.0下): 第一部分 服务器端 一、创建服务器套接字(create)。 二、服务器套接字进行信息绑定(bind),并开始监听连接(listen)。 三、接受来自用户端的连接请求(accept)。 四、开始数据传输(send/receive)。 五、关闭套接字(closesocket)。 第二部分 客户端 一、创建客户套接字(create)。 二、与远程服务器进行连接(connect),如被接受则创建接收进程。 三、开始数据传输(send/receive)。 四、关闭套接字(closesocket)。 CSocket的编程步骤:(注意我们一定要在创建MFC程序第二步的时候选上Windows Socket选项,其中ServerSocket是服务器端用到的,ClientSocket是客户端用的。) (1)构造CSocket对象,如下例: CSocket ServerSocket; CSocket ClientSocket; (2)CSocket对象的Create函数用来创建Windows Socket,Create()函数会自行调用Bind()函数将此Socket绑定到指定的地址上面。如下例: ServerSocket.Create(823); //服务器端需要指定一个端口号,我们用823。 ClientSocket.Create(); //客户端不用指定端口号。 (3)现在已经创建完基本的Socket对象了,现在我们来启动它,对于服务器端,我们需要这个Socket不停的监听是否有来自于网络上的连接请求,如下例: ServerSocket.Listen(5);//参数5是表示我们的待处理Socket队列中最能有几个Socket。 (4)对于客户端我们就要实行连接了,具体实现如下例: ClientSocket.Connect(CString SerAddress,Unsinged int SerPort);//其中SerAddress是服务器的IP地址,SerPort是端口号。 (5)服务器是怎么来接受这份连接的呢?它会进一步调用Accept(ReceiveSocket)来接收它,而此时服务器端还须建立一个新的CSocket对象,用它来和客户端进行交流。如下例: CSocket ReceiveSocket; ServerSocket.Accept(ReceiveSocket); (6)如果想在两个程序之间接收或发送信息,MFC也提供了相应的函数。如下例: ServerSocket.Receive(String,Buffer); //String是你要发送的字符串,Buffer是发送字符串的缓冲区大小。ServerSocket.Send(String,Butter);//String是你要接收的字符串,Buffer是接收字符串的缓冲区大小。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值