欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

计算机网络课程设计之网络聊天程序的设计与实现

程序员文章站 2024-02-12 10:44:52
...

TCP和UDP在接收方的区别(实际上本实验中用不着)

UDP 协议(User Datagram Protocol 用户数据报协议),是一 种保护消息边界的,不保障可靠数据的传输。就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的 消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包

TCP 协议(Transmission Control Protocol 传输控制协议), 是一种流传输的协议。他提供可靠的、有序的、双向的、面向连接的传输。 如果发送端连续发送数据,接收端有可能在一次接收动作中, 会接收两个或者更多的数据包

举例来说,假如,我们连续发送三个数据包,大小分别是 2k、4k、8k,这三个数据包都已经到达 了接收端的网络堆栈中,如果使用 UDP 协议,不管我们使用多大的接收缓冲区去接收数据,我们必须 有三次接收动作,才能够把所有的数据包接收完。而使用 TCP 协议,我们只要把接收的缓冲区大小设 置在 14k 以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作。

正文:实验部分

思路:具体学习参考B站大佬,他的代码无法实现全双工,原因是Socket开启了阻塞,我改成非阻塞之后就出现了意料之外的错误,由于后面还有别的实验要做,这里就没修改了。
先贴一份实现简易聊天功能的代码,主要是注解。这是实现聊天室的基础
#include "pch.h"
#include <stdio.h> 
#include <Winsock2.h> 

#pragma comment( lib, "WSOCK32.LIB")

void main() {


	printf("服务器 SocketDLL初始化");
	/****** SocketDLL的初始化 ******/
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	wVersionRequested = MAKEWORD(1, 1);
	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) { return; }
	if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
	{
		WSACleanup();   return;
	}

	/**创建服务器Socket**/

	// AF_INET : 使用IP地址族
	// 套接字类型为SOCK_STREAM流类型, 也就是适用于TCP
	// UDP的话是SOCK_DGRAM 数据报套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);



	/*****初始化服务器的包结构********/

	// 封装成sockaddr_in 类,俗称地址包,保存端口号和IP地址
	SOCKADDR_IN addrSrv;
	// 将IP地址 INADDR_ANY (0x00000)主机字节转换成网络字节:高字节在前面
	//addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	// 我们弃用这种默认值,使用局域网中的IP
	char ip[] = "127.0.0.1";
	// inet_addr:将点分十进制的IP 转换成二进制,然后再转换成网络字节
	addrSrv.sin_addr.S_un.S_addr = inet_addr(ip);
	// 设置为IP协议族
	addrSrv.sin_family = AF_INET;
	// 设置端口号,把主机字节转换成网络字节
	addrSrv.sin_port = htons(6000);


	/***将服务器Socket和服务器包进行绑定*****/

	// 强制转换的作用在于:SOCKADDR_IN 地址包的形式,只是方便了我们设置参数
	// 而使用的时候,bind还是需要把这些参数揉和到一起。
	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));


	// 让服务器Socket开启监听,并且设置最大的等待连接数
	// 等待连接数(半连接)过大会给服务器造成负载
	listen(sockSrv, 5);

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);
	while (1) {
		SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		char sendBuf[50];
		// inet_ntoa 是把二进制IP转换成点分十进制,是上面那个的逆
		// 调用springf将字符串转换成 适合传输的类型
		sprintf(sendBuf, "Welcome %s to here!", inet_ntoa(addrClient.sin_addr));
		// 进行发操作
		send(sockConn, sendBuf, strlen(sendBuf) + 1, 0);
		char recvBuf[50];
		// 接收操作
		recv(sockConn, recvBuf, 50, 0);
		printf("%s\n", recvBuf);
		closesocket(sockConn);
	}
}

简易客户端

#include <stdio.h>
#include <Winsock2.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return;
}
if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return;
}
SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
send(sockClient,"hello",strlen("hello")+1,0);
char recvBuf[50];
recv(sockClient,recvBuf,50,0);
printf("%s\n",recvBuf);
closesocket(sockClient);
WSACleanup();
}

下面的代码是我写的实现全双工和多线程处理,不过存在BUG

客户端


#include "pch.h"
#include <Winsock2.h> 
#include <cstdlib>
#include <stdio.h> 
#include <iostream>

#pragma comment(lib, "ws2_32.lib")

using namespace std;

// 全局常量
const int SEND_SIZE = 1000;
const int MAX_BUF_SIZE = 200;


// 全局变量
SOCKET sockClient;
SOCKADDR_IN addrSrv;

// 接收线程的设置是死循环不断得提交recv申请,如果有反馈,就输出。
DWORD WINAPI Client_Receive_Thread(LPVOID lp) {
	SOCKET *s = (SOCKET*)lp;
	int nrecv;
	while (true)
	{
		// 监听服务器端消息
		char recvBuf[SEND_SIZE];
		// recv 的第一个参数是当前socket
		int res = recv(sockClient, recvBuf, SEND_SIZE, 0); // 最后参数设置成0,表示非阻塞
		if (res > 0) // 由于socket默认的阻塞,因此recv会自动阻塞
		{
			printf("%s\n", recvBuf);

		}
	}
}

void main() {
	printf("客户端 SocketDLL初始化");
	/****** SocketDLL的初始化 ******/
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	wVersionRequested = MAKEWORD(1, 1);
	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) { return; }
	if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
		WSACleanup();   return;
	}


	sockClient = socket(AF_INET, SOCK_STREAM, 0);
	unsigned long ul = 1;
	int ret = ioctlsocket(sockClient, FIONBIO, (unsigned long *)&ul);
	if (ret == SOCKET_ERROR) {
		printf("非阻塞化失败!");
		return;
	}
	/****设置地址包*****/
	// 巡回测试IP
	addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.86");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);

	char username[50];
	printf("请输入你的姓名: ");
	scanf("%s", username);


	for(int i=1;i<=5;i++)// 非阻塞下,不知道如何建立连接了。
	{
		cout << "尝试发送连接请求...." << endl;
		// 发起到服务器的连接,第二个参数设置了服务器端的参数
		int ret = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
		if (ret == 0)
		{
			cout << "连接成功" << endl;
			// 建立连接,且第一次发送username
			send(sockClient, username, SEND_SIZE, 0);
			break;
		}
		cout << "连接失败" << endl;
	}
	

	// 客户端开启一个线程,负责监听服务器端
	LPVOID *lp = (LPVOID*)&sockClient;
	HANDLE hThread = CreateThread(NULL, 0, Client_Receive_Thread, lp, 0, NULL);

	// 主线程用于输入
	char input[50];
	char msg[MAX_BUF_SIZE];
	while (true)
	{

		scanf("%s", input);

		if (strcmp(input, "exit") == 0)
		{
			printf("bye\n");
			break;
		}
		else if (strcmp(input, "send") == 0) {
			scanf("%s", msg);
			printf("success send msg: %s\n", msg);
			send(sockClient, msg, SEND_SIZE, 0);
		}
	}
	// 关闭连接
	closesocket(sockClient);
	WSACleanup();
}

服务器端

#include "pch.h"
#include <stdlib.h>
#include <stdio.h>
#include <WinSock2.h>
#include <iostream>
#include <WS2tcpip.h>
#include <cstring>

#pragma comment(lib,"ws2_32.lib")
using namespace std;



// 常量

const int SEND_SIZE = 1000;
const int MAX_BUF_SIZE = 500;
const int NICKNAME_LEN = 20;
const int MAX_CLIENT_COUNT = 20;

// 变量

int clientCount = 0;



// 封装成Client结构
struct Client
{
	SOCKET s;
	SOCKADDR_IN sin;
	char name[NICKNAME_LEN];
}Cli[MAX_CLIENT_COUNT];



DWORD WINAPI ServerListeningConnect(LPVOID lp) {
	SOCKET *s = (SOCKET*)lp;
	int nrecv;
	int len = sizeof(SOCKADDR);
	while (true)
	{
		Cli[clientCount + 1].s = accept(*s, (SOCKADDR*)&Cli[clientCount + 1].sin, &len);

		// 这里不清楚是否是阻塞,因此加上判断
		if (Cli[clientCount + 1].s != INVALID_SOCKET)
		{
			clientCount++;

			unsigned long ul = 1;
			int ret = ioctlsocket(Cli[clientCount].s, FIONBIO, (unsigned long *)&ul);
			if (ret == SOCKET_ERROR) {
				printf("非阻塞化失败!");
				return 0;
			}

			// 获取客户端的姓名
			recv(Cli[clientCount].s, Cli[clientCount].name, SEND_SIZE  , 0);
			 
			// 反馈。不晓得能反馈到哪
			
			 
			for (int i = 1; i <= clientCount; i++)
			{
				char sendBuf[MAX_BUF_SIZE] = "Welcome [";
				strcat(sendBuf, Cli[i].name);
				strcat(sendBuf, "] 加入直播间,当前直播间有");
				char number[50];
				itoa(clientCount, number, 50);
				strcat(sendBuf, number);
				strcat(sendBuf, "人");
				printf("%s\n", sendBuf);
				send(Cli[i].s, sendBuf, SEND_SIZE, 0);
				
			}
			

			
		}

	}
}

int main()
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	wVersionRequested = MAKEWORD(1, 1);
	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) {
		return 0 ;
	}
	if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
		WSACleanup();
		return 0;
	}

	printf("初始化成功。。。。\n");
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
	unsigned long ul = 1;
	int ret = ioctlsocket(sockSrv, FIONBIO, (unsigned long *)&ul);
	if (ret == SOCKET_ERROR) {
		printf("非阻塞化失败!");
		return 0;
	}

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.86");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);
	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
	listen(sockSrv, 5);


	// 开启线程监听连接

	LPVOID *lp = (LPVOID*)&sockSrv;
	HANDLE hThread = CreateThread(NULL, 0, ServerListeningConnect, lp, 0, NULL);


	// 开启线程监听是否客户端发送了消息
	//就写在主线程吧
	// 
	
	while (true)
	{

		char recvbuf[MAX_BUF_SIZE];
		for (int i = 1; i <= clientCount; i++)
		{
			// 当收到消息时。这里有点担心recv是阻塞的,这样的话收到第一个后,就会卡住
			if (recv(Cli[i].s, recvbuf, sizeof(recvbuf), 0) != INVALID_SOCKET)
			{
				// 向另外的客户端进行发送
				char sendbuf[SEND_SIZE];
				strcpy(sendbuf, "[");
				strcat(sendbuf, Cli[i].name);
				strcat(sendbuf, "]:>");
				strcat(sendbuf, recvbuf);
				for (int j = 1; j <= clientCount; j++)
				{
					if (j == i)
						continue;
					
					printf("服务器接收到了来自%d 发给 %d  的 %s\n", i,j,recvbuf);
					printf("服务器即将发送 %s\n", sendbuf);
					send(Cli[j].s, sendbuf, SEND_SIZE, 0);
				}
			}
		}
	}
}