首页 > 编程语言 >利用Socket,编写一个聊天程序

利用Socket,编写一个聊天程序

时间:2024-10-16 12:46:17浏览次数:3  
标签:Socket int 消息 聊天 服务器 编写 接字 连接 客户端

实验1:利用Socket,编写一个聊天程序

实验要求

要求

1.给出聊天协议的完整说明;
2.利用 C 或 C++ 语言,使用基本的 Socket 函数完成程序。不允许使用 CSocket 等封装后的类编写程序;
3.使用流式套接字、采用多线程(或多进程)方式完成程序;
4.程序应该有基本的对话界面,但可以不是图形界面。程序应该有正常的退出方式;
5.完成的程序应该支持多人聊天,支持英文和中文聊天;
6.编写的程序应该结构清晰,具有较好的可读性;
7.在实验中观察是否有数据丢失,提交可执行文件、程序源码和实验报告。

概念解析

什么是聊天协议?

聊天协议是一种定义了客户端和服务器之间如何进行通信的规则集合。它规定了消息格式、编码方式、传输机制等,确保了双方能够正确地发送和接收信息。一个简单的聊天协议可能包括以下几个方面:
1.连接建立:客户端向服务器发起连接请求。
2.身份验证:用户登录时需要提供用户名(或昵称)以及密码等信息,服务器验证这些信息的有效性。
3.消息格式:定义消息的数据结构,比如使用JSON、XML或者自定义二进制格式来表示一条消息。
4.命令类型:除了文本消息外,还可能有系统命令如加入房间、退出房间、获取在线用户列表等。
5.错误处理:定义当发生错误时的消息格式及应对措施。
6.断开连接:客户端可以正常关闭连接或异常断开,服务器需要妥善处理这种情况。

基本的 Socket 函数
  1. socket
    功能:创建一个新的套接字。
    原型:int socket(int domain, int type, int protocol);
    参数:
    domain:地址族,如 AF_INET(IPv4)或 AF_INET6(IPv6)。
    type:套接字类型,如 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP)。
    protocol:协议类型,通常为0,表示使用默认协议。
    返回值:成功时返回套接字描述符,失败时返回-1。
  2. bind
    功能:将套接字绑定到一个特定的地址和端口。
    原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    参数:
    sockfd:套接字描述符。
    addr:指向包含地址信息的结构体指针(如 struct sockaddr_in)。
    addrlen:地址结构体的长度。
    返回值:成功时返回0,失败时返回-1。
  3. listen
    功能:将套接字设置为监听状态,准备接受连接请求。
    原型:int listen(int sockfd, int backlog);
    参数:
    sockfd:套接字描述符。
    backlog:等待连接队列的最大长度。
    返回值:成功时返回0,失败时返回-1。
  4. accept
    功能:从监听队列中接受一个连接请求。
    原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    参数:
    sockfd:监听套接字描述符。
    addr:指向存储客户端地址信息的结构体指针。
    addrlen:指向存储地址结构体长度的变量的指针。
    返回值:成功时返回新的套接字描述符,失败时返回-1。
  5. connect
    功能:发起一个连接请求到指定的服务器。
    原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    参数:
    sockfd:套接字描述符。
    addr:指向包含服务器地址信息的结构体指针。
    addrlen:地址结构体的长度。
    返回值:成功时返回0,失败时返回-1。
  6. send 和 recv
    功能:发送和接收数据。
    原型:
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    参数:
    sockfd:套接字描述符。
    buf:指向要发送/接收的数据缓冲区的指针。
    len:要发送/接收的数据长度。
    flags:选项标志,通常为0。
    返回值:成功时返回实际发送/接收的字节数,失败时返回-1。
  7. sendto 和 recvfrom
    功能:用于无连接的套接字(如UDP),发送和接收数据。
    原型:
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    参数:
    sockfd:套接字描述符。
    buf:指向要发送/接收的数据缓冲区的指针。
    len:要发送/接收的数据长度。
    flags:选项标志,通常为0。
    dest_addr / src_addr:指向目标/源地址信息的结构体指针。
    addrlen / *addrlen:地址结构体的长度。
    返回值:成功时返回实际发送/接收的字节数,失败时返回-1。
  8. close
    功能:关闭套接字。
    原型:int close(int fd);
    参数:
    fd:文件描述符(套接字描述符)。
    返回值:成功时返回0,失败时返回-1。
什么是流式套接字?什么是多线程?

流式套接字(Stream Sockets)是一种提供面向连接的、可靠的、双向的、基于字节流的通信服务的套接字类型。在TCP/IP协议族中,流式套接字通常使用传输控制协议(TCP)来实现。
主要特点
1.面向连接:在数据传输之前,必须先建立一个连接。这意味着客户端和服务器需要通过三次握手过程来建立一个会话。
2.可靠传输:TCP协议提供了可靠的数据传输,确保数据包按顺序到达,并且没有丢失或损坏。如果数据包在传输过程中丢失或损坏,TCP会自动重传这些数据包。
3.双向通信:一旦连接建立,双方都可以发送和接收数据。
4.字节流:数据以字节流的形式传输,没有消息边界。应用程序需要自己处理消息的分界和重组。

多线程是一种编程和执行模型,它允许一个程序同时执行多个线程。每个线程都是程序中的一个独立执行路径,可以并行运行。多线程的主要目的是提高程序的性能和响应性,特别是在处理I/O密集型或计算密集型任务时。

实验过程

语言:C++

聊天协议

消息格式
消息内容:实际的消息内容。

消息类型
默认是普通聊天信息,输入quit退出。

消息结构
每条消息包含客户端 ID 和实际消息内容

连接建立
三次握手:客户端与服务器之间通过 TCP 的三次握手建立连接。
客户端连接:客户端发送连接请求到服务器。
服务器响应:服务器接受连接请求,并分配一个唯一的客户端 ID。

消息传输
发送消息:客户端发送消息时,先将消息格式化为 ID:消息内容\n 的形式,然后通过 send 函数发送给服务器。
接收消息:服务器接收消息后,解析出客户端 ID 和消息内容,然后广播给所有其他客户端。
广播消息:服务器将消息转发给所有已连接的客户端(不包括发送者)。

连接关闭
四次挥手:客户端或服务器主动关闭连接时,通过 TCP 的四次挥手断开连接。
退出命令:客户端发送 quit 命令时,服务器关闭对应的套接字,并从客户端列表中移除该客户端。

服务器

1.监听客户端连接请求。
2.为每个新连接创建一个新的线程来处理该客户端的消息。
3.维护一个客户端列表,用于管理和广播消息。

#define _WINSOCK_DEPRECATED_NO_WARNINGS
//函数inet_addr过时,会有弃用警告
//这里定义 WINSOCK_DEPRECATED_NO_WARNINGS 宏,禁用弃用警告

#define _CRT_SECURE_NO_WARNINGS
//函数sprintf不安全,在处理字符串时可能会导致缓冲区溢出,从而引发安全问题。

#include<iostream>
#include <winsock2.h>
#include <windows.h>


#pragma comment(lib, "ws2_32.lib")
//#pragma comment(lib, "ws2_32.lib") 是一个预处理指令,用于在编译时自动链接指定的库文件。
//在这个例子中,它告诉编译器在链接阶段链接 ws2_32.lib 库,这是 Windows Sockets 2 (Winsock 2) 的库文件。

using namespace std;

#define NUM 1024
SOCKET clientSocket[NUM];

int c;//如果变量名为count会报错,std命名空间里有std::count,与全局变量count冲突

void communication(int index) 
{
	int r;
	char buff[56];
	char temp[80];
	while (1)
	{
		r = recv(clientSocket[index], buff, 55, NULL);
		/*
		如果未发生错误, recv 将返回收到的字节数, buf 参数指向的缓冲区将包含接收的此数据。 
		如果连接已正常关闭,则返回值为零。
		否则,将返回值 SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
		*/


		if (r > 0)
		{
			buff[r] = 0;//添加结束符号
			cout << "客户端 " << index << " :" << buff << endl;
		}

		// 检查是否接收到 "quit"
		if (strcmp(buff, "quit") == 0) 
		{
			cout << "客户端 " << index << " 退出" << endl;
			break;  // 跳出循环
		}


		memset(temp, 0, 80);//使用 memset 清空 temp 缓冲区
		sprintf(temp, "%d:%s", index, buff);

		//发给当前所有连上服务器的客户端
		for (int i = 0; i < c; i++) 
		{
			send(clientSocket[i], temp, strlen(temp), NULL);
		}
		//遍历所有客户端套接字 clientSocket,使用 send 函数将格式化后的消息 temp 广播给每个客户端。


	}
}


int main()
{
	c = 0;
	//1.确定网络协议版本
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);//选中WSAStartup,按F1,查看在线帮助文档
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		cout << ("确定网络协议版本失败: ") << GetLastError() << endl;
		system("pause");
		return -1;
	}
	cout << ("确定网络协议版本成功") << endl;

	//2.创建socket
	SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (SOCKET_ERROR == serverSocket)
	{
		cout << ("创建socket失败: ") << GetLastError() << endl;

		//清除协议版本信息
		WSACleanup();

		system("pause");
		return -1;
	}
	cout << ("创建socket成功") << endl;

	//3.确定服务器协议地址簇
	SOCKADDR_IN addr = { 0 };
	addr.sin_family = AF_INET;

	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//IP地址(区分计算机)
	//inet_addr 是一个用于将点分十进制格式的 IPv4 地址字符串
	//转换为 32 位无符号长整型(in_addr_t)网络字节序表示的函数。
	//在 Windows Sockets (Winsock) API 中定义,通常用于网络编程中处理 IP 地址。

	addr.sin_port = htons(9527);//端口号(区分同一计算机上不同软件)



	//4.绑定
	int r = bind(serverSocket, (sockaddr*)&addr, sizeof addr);

	if (-1 == r)
	{
		cout << ("绑定失败: ") << GetLastError() << endl;

		//关闭socket
		closesocket(serverSocket);
		//清除协议版本信息
		WSACleanup();

		system("pause");
		return -1;
	}
	cout << ("绑定成功") << endl;

	//5.监听
	r = listen(serverSocket, 10);
	if (-1 == r)
	{
		cout << ("监听失败: ") << GetLastError() << endl;

		//关闭socket
		closesocket(serverSocket);
		//清除协议版本信息
		WSACleanup();

		system("pause");
		return -1;
	}
	cout << ("监听成功") << endl;

	//6.接受客户端连接
	for (int i = 0; i < NUM; i++) 
	{
		clientSocket[i] = accept(serverSocket, (sockaddr*)NULL, NULL);
		if (-1 == r)
		{
			cout << ("服务器崩溃: ") << GetLastError() << endl;

			//关闭socket
			closesocket(serverSocket);
			//清除协议版本信息
			WSACleanup();

			system("pause");
			return -1;
		}
		cout << "客户端 " << i << " 接入服务器" << endl;

		c++;

		CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)communication, (LPVOID)i, NULL, NULL);
	}



	//7.通信
	//char buff[56];
	//while (1)
	//{
	//	r = recv(clientSocket[1], buff, 55, NULL);
	//	if (r > 0)
	//	{
	//		buff[r] = 0;//添加结束符号
	//		cout << ">>" << buff << endl;
	//	}
	//}

	//8.关闭socket
	closesocket(serverSocket);

	//9.清理协议版本信息
	WSACleanup();

	while (0);
	return 0;
}

客户端

1.连接到服务器。
2./在一个线程中读取用户输入并发送给服务器。
3.在另一个线程中接收并显示来自服务器的消息。

#define _WINSOCK_DEPRECATED_NO_WARNINGS
//函数inet_addr过时,会有弃用警告
//这里定义 WINSOCK_DEPRECATED_NO_WARNINGS 宏,禁用弃用警告

#define _CRT_SECURE_NO_WARNINGS
//scanf 函数不安全,可能会导致缓冲区溢出等安全问题

#include<iostream>
#include<stdio.h>
#include <winsock2.h>
#include <graphics.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;

SOCKET serverSocket;


void mySend() 
{
	//循环接受用户输入,发送给服务器
	char buff[56];
	while (1)
	{
		if (fgets(buff, sizeof(buff), stdin) != NULL) {
			// 去除换行符
			buff[strcspn(buff, "\n")] = 0;
			
			// 显示输入的字符串
			//printf("你输入的是: %s\n", buff);

		}
		else {
			// 读取失败
			printf("输入失败,请确保输入的是有效的字符串。\n");
		}
		
		//scanf("%s", buff);

		send(serverSocket, buff, strlen(buff), NULL);
		if (strcmp(buff, "quit") == 0)
		{
			exit(0);
		}
	}
}

int main()
{
	initgraph(400, 600,SHOWCONSOLE);//窗口
	int len = 0;



	//1.确定网络协议版本
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);//选中WSAStartup,按F1,查看在线帮助文档
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		cout << ("确定网络协议版本失败: ") << GetLastError() << endl;
		system("pause");
		return -1;
	}
	cout << ("确定网络协议版本成功") << endl;

	//2.创建socket
	serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (SOCKET_ERROR == serverSocket)
	{
		cout << ("创建socket失败: ") << GetLastError() << endl;

		//清除协议版本信息
		WSACleanup();

		system("pause");
		return -1;
	}
	cout << ("创建socket成功") << endl;

	//3.确定服务器协议地址簇
	SOCKADDR_IN addr = { 0 };
	addr.sin_family = AF_INET;

	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//IP地址(区分计算机)
	//inet_addr 是一个用于将点分十进制格式的 IPv4 地址字符串
	//转换为 32 位无符号长整型(in_addr_t)网络字节序表示的函数。
	//在 Windows Sockets (Winsock) API 中定义,通常用于网络编程中处理 IP 地址。

	addr.sin_port = htons(9527);//端口号(区分同一计算机上不同软件)



	//4.连接服务器
	int r = connect(serverSocket, (sockaddr*)&addr, sizeof addr);
	if (-1 == r)
	{
		cout << ("连接服务器失败: ") << GetLastError() << endl;

		//关闭socket
		closesocket(serverSocket);
		//清除协议版本信息
		WSACleanup();

		system("pause");
		return -1;
	}
	cout << ("连接服务器成功") << endl;
	cout << "==============================================" << endl;
	cout << "欢迎来到仙境之桥聊天室!(输入 quit 退出聊天室)" << endl;
	cout << "==============================================" << endl;

	//5.通信
	CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)mySend, NULL, NULL, NULL);
	char temp[60];
	while(1)
	{
		//接受服务器发来的数据并显示
		r = recv(serverSocket, temp, 59, NULL);
		if (r > 0) 
		{
			temp[r] = 0;
			outtextxy(1, len * 20, temp);
			len++;
		}

	}

	//6.关闭socket
	closesocket(serverSocket);

	//7.清理协议版本信息
	WSACleanup();

	while (0);
	return 0;
}

总结

测试观察

测试环境:在本地机器上运行服务器和多个客户端进行测试。
测试用例:
1.多个客户端同时连接并发送消息。
2.客户端发送长消息和短消息。
3.客户端发送中文和英文消息。
4.客户端正常退出和异常退出。
观察结果:
1.所有消息都能正确地在客户端之间传递。
2.中文和英文消息都能正确显示。
3.客户端退出时,服务器能正确处理并更新客户端列表。
4.未观察到数据丢失现象。

1
2

反思

成功点:
1.成功实现了基于 TCP 协议的多人聊天系统。
2.支持多用户同时在线聊天,支持英文和中文消息。
3.使用多线程处理客户端连接,提高了系统的并发性能。
改进点:
1.可以增加更多的错误处理和日志记录功能,提高系统的健壮性。
2.可以优化消息缓冲机制,进一步减少消息分割的可能性。
3.可以增加图形界面或更友好的文本界面,提升用户体验。

通过本次实验,我掌握了基本的 Socket 编程技术和多线程编程方法,理解了 TCP 协议的工作原理,并成功实现了一个简单的多人聊天系统。

我是彩笔,仅供参考!
你不会直接ctrl+cv的对吧!

标签:Socket,int,消息,聊天,服务器,编写,接字,连接,客户端
From: https://blog.csdn.net/m0_74755033/article/details/142978767

相关文章

  • 1.5K+ Star!assistant-ui:一套构建AI聊天界面的组件库
    assistant-ui简介assistant-ui[1]是一套用于构建AI聊天界面的React组件库。它集成了多种模型提供商,如OpenAI、Anthropic、AWS、Google等,并支持自定义API集成。它旨在简化AI聊天界面的开发过程,使开发者能够快速构建出功能丰富的聊天应用。项目特点主要特点模型提供商支......
  • 第九章习题3-编写一个函数print,打印一个学生的成绩数组,该数组有5个学生的数据记录,每个
     ......
  • socket实现简单ssh服务(解决socket粘包)
    1.服务端importsocket,osserver=socket.socket()server.bind(('localhost',22222))server.listen()whileTrue:conn,addr=server.accept()print("newconn:",addr)whileTrue:data=conn.recv(1024)ifnotdata:......
  • MQTTnet.Server同时支持mqtt及websocket协议
    Net6后写法 Net6前写法Program.csusingMicrosoft.AspNetCore.Hosting;usingMicrosoft.Extensions.Configuration;usingMicrosoft.Extensions.Hosting;usingMQTTnet.AspNetCore;usingSystem;usingSystem.IO;namespaceMQTTnet.Server{publicclassProgra......
  • Chromium 中HTML5 WebSocket收发消息分析c++(一)
    一、WebSocket前端接口定义:WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的API。使用 WebSocket() 构造函数来构造一个 WebSocket。构造函数WebSocket(url[,protocols])返回一个 WebSocket 对象。常量ConstantValueWeb......
  • Chromium 中HTML5 WebSocket收发消息分析c++(二)
    看下websocket调用过程:基本定义参考上一篇:Chromium中HTML5WebSocket收发消息分析c++(一)-CSDN博客一、前端测试用例 参考:HTML5WebSocket|菜鸟教程(runoob.com) websocket.html文件如下:<!DOCTYPEHTML><html><head><metacharset="utf-8"><title>Web......
  • 4. WebSockets
    4.WebSockets4.1.WebSocket介绍WebSocket协议RFC6455提供了一种标准化方法,可以通过单个TCP连接在Client端和服务器之间构建全双工双向通信通道。它是与HTTP不同的TCP协议,但旨在通过端口80和443在HTTP上工作,并允许重复使用现有的防火墙规则。WebSocket交互......
  • 一款由AI编写,简洁而实用的开源IP信息查看器
    大家好,今天给大家分享一款用于查询和显示用户当前IP地址的轻量级项目MyIP。MyIP提供了多种功能,包括IP地址查询、网络连通性检查、WebRTC连接检测、DNS泄露检查、网速测试、MTR测试等等。使用MyIP,我们可以轻松地查看自己的公网IP地址,并且可以方便地进行网络诊断或监控。项......
  • 【油猴脚本】00027 案例 Tampermonkey油猴脚本, 仅用于学习,不要乱搞。添加标题为网页数
    前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦......
  • javaweb实现下载功能报错sockettimeout
    javaweb压缩zip包下载,并响应头里面指定文件大小在JavaWeb应用程序中,如果你想要创建一个ZIP文件并通过HTTP响应提供下载,并且希望在响应头中指定文件大小,你可以先将文件写入到一个临时的ByteArrayOutputStream中,这样你就可以计算出压缩后的文件大小。然后,你可以将这个字节......