计算机网络课程设计之网络聊天程序的设计与实现
程序员文章站
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);
}
}
}
}
}
上一篇: git快速搭建文件目录
下一篇: 思科(Cisco)交换机路由器命令大全