基于TCP/UDP的简单客户端和服务端通讯程序


基于TCP/UDP的简单客户端和服务端通讯程序

一、TCP

1.TCP原理

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于流的通信协议。它是互联网协议栈(TCP/IP)中的核心协议之一,主要用于保证在计算机网络中可靠地传输数据。

TCP通信的基本特点:

面向连接:在发送数据之前,TCP要求通信双方(客户端和服务器)首先建立一个连接,这个过程被称为“三次握手”。连接建立后,数据才可以传输;数据传输完成后,需要释放连接(通过“四次挥手”关闭连接)。

可靠传输:TCP保证数据包的正确传输。通过序列号和确认号的机制,TCP能够检测丢包、乱序、重复等问题,并通过重传机制进行纠正,从而确保数据的完整性和顺序性。

基于流:TCP传输的数据没有消息边界,而是一个连续的数据流。数据可以按照任意大小进行发送和接收,应用层必须根据协议或约定来解析数据边界。

众所周知,tcp是可靠传输,这个可靠就是指tcp通信是面向连接的通信,所以客户端需要知道服务端的协议族、ip地址和端口号。

协议族就是选择IPV4 或者 IPV6

IP地址由四段组成,每个字段是一个字节,即4个字节、 每个字节有8位,最大值是255(=256:0~255),在你的电脑上的命令行窗口上输入命令 ipconfig 即可查看本机的ip地址

端口号(Port Number)是网络传输层协议(如TCP或UDP)用来识别特定服务或应用程序的数字标识符。每个IP地址 可以有多个端口号,每个端口号代表不同的服务。

如下代码所示即为设置协议族、ip地址和端口号

1
2
3
4
sockaddr_in sin;
sin.sin_family = AF_INET;//设置地址族为IPv4
sin.sin_port = htons(5678);//设置端口号为5678,使用网络字节顺序
sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//将IP地址设置为本地地址

image-20240922104634246

暂且不细谈三次握手和四次挥手

用简单通俗的语言来解释就是

三次握手:

①客户端 向 服务端 发送连接请求的消息

②服务端 收到 客户端的请求后 发送一次 ‘收到’ 消息

③客户端 收到 服务端的‘收到’ 消息后 回复一次 ‘确认收到’ 的消息

四次挥手:

①客户端 向 服务端发送 ’请求断开连接的消息‘

②服务端 回复’收到请求断开连接的消息‘

③服务端 将剩下的内容全部收拾完毕后 再次发送 ‘我已经准备好了’ 的消息

④客户端在接收到信息后,便可正常断开连接了

TCP程序中的发送/接受信息的函数是:

image-20240922114548643

image-20240922114615523

2.Init.h的头文件解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once
#include <winsock2.h>
#pragma comment(lib, "WS2_32")
class CInitSock
{
public:
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
~CInitSock()
{
::WSACleanup();
}
};

这个头文件相当于一个工具类,是用于初始化和清理 Winsock 库的 C++ 类 CInitSock

编写程序的之后只需要进行一次CInitSock类的创建即可,由于构造函数和析构函数的存在,便可以实现对象构造时初始化 Winsock 库,在对象销毁时清理 Winsock 资源,方便进行后续的网络编程操作

2.TCP服务端程序设计

Server.cpp主要目的:创建一个TCP服务器,监听端口4567,并在客户端连接时向客户端发送一条消息

TCP的服务端首先创建一个socket对象( sListen

然后使用 bind 函数 将以下的地址信息绑定到sListen

1
2
3
4
sockaddr_in sin;
sin.sin_family = AF_INET;//设置地址族为IPv4
sin.sin_port = htons(4567);//设置端口号为4567,使用网络字节顺序
sin.sin_addr.S_un.S_addr = INADDR_ANY;//将IP地址设置为任意本地地址

服务端不断监听来自其他程序的发送到此端口上的连接请求信息,只要接收到了连接请求,便可以进行通信

服务端是不需要知道客户端的地址信息的,服务端只需要不断监听连接请求

服务端完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "Init.h"
#include <stdio.h>
CInitSock initSock;


int main()
{
printf("***********服务端 Server *****************/n");
//创建一个监听套接字 sListen,使用TCP协议进行通信
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET)
{
printf("Failed socket()/n");
return 0;
}
//设置服务器地址 sin
sockaddr_in sin;
sin.sin_family = AF_INET;//设置地址族为IPv4
sin.sin_port = htons(4567);//设置端口号为4567,使用网络字节顺序
sin.sin_addr.S_un.S_addr = INADDR_ANY;//将IP地址设置为任意本地地址

//绑定套接字到服务器地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("Failed bind()/n");
return 0;
}

//开始监听连接请求
if (::listen(sListen, 2) == SOCKET_ERROR)
{
printf("Falied listen()/n");
return 0;
}

//准备接受远程客户端的连接
sockaddr_in remoteAddr;//创建 sockaddr_in 类型的 remoteAddr 用于存储远程客户端的地址信息
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient;
char szText[] = "你好!我是服务端!欢迎对话!/n"; //创建一个用于发送给客户端的消息字符串 szText

//等待客户端连接并接受连接请求
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (sClient == INVALID_SOCKET)
{
printf("Failed accept()");
return 0;
}
printf(" Receive a conection: %s/r/n", inet_ntoa(remoteAddr.sin_addr));

char buff[256];

while (TRUE)
{
::send(sClient, szText, strlen(szText), 0);//向客户端发送消息
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '/0';
printf("服务器Server端收到客户端Client信息如下: %s/n", buff);
}

}
::closesocket(sListen);//关闭与客户端的连接
return 0;
}

3.TCP客户端程序设计

Client.cpp主要目的:向已知地址信息的服务端发送连接请求,并在连接成功后收到信息时,回复一条消息

客户端必须要知道服务端正确的地址信息,才能正确的发送连接请求

客户端首先创建一个 SOCKET 对象,代表客户端自己

接着创建 sockaddr_in 对象,这个对象代表服务端程序,所以需要设置完整的地址信息

1
2
3
4
sockaddr_in servAddr;//定义服务器地址 servAddr
servAddr.sin_family = AF_INET; //设置地址族为IPv4
servAddr.sin_port = htons(4567);//将客户端要连接的服务器端口号设置为 4567
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

这个代表要发送连接请求到ip地址为 “127.0.0.1” 的主机上(若要实现两台电脑进行通信,可以继续往下看)

发送连接请求

1
2
3
4
5
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
printf("Failed connect()/n");
return 0;
}

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "Init.h"
#include <stdio.h>
#include<iostream>
using namespace std;
CInitSock initSock;

int main()
{
printf("***********客户端 Client *****************/n");
//创建一个套接字 s,使用TCP协议进行通信
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
printf("Failed socket()/n");
return 0;
}
sockaddr_in servAddr;//定义服务器地址 servAddr
servAddr.sin_family = AF_INET; //设置地址族为IPv4
servAddr.sin_port = htons(4567);//将客户端要连接的服务器端口号设置为 4567
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//将IP地址设置为本地主机(127.0.0.1)
//servAddr.sin_addr.S_un.S_addr = inet_addr("10.212.21.19");

//尝试与服务器建立连接
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
printf("Failed connect()/n");
return 0;
}

char buff[256];
int nRecv = ::recv(s, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '/0';
printf(" Receive Data from Server: %s", buff);
}

while (true)
{
char sendData[] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
printf("请输入要发送的信息内容: ");
cin >> sendData;
int nSent = ::send(s, sendData, strlen(sendData), 0);
if (nSent == SOCKET_ERROR)
{
printf("发送失败/n");
}
else
{
printf("*********成功发送信息,信息内容: %s *********/n", sendData);
}

}

::closesocket(s);
return 0;
}

4.两台主机进行通信

一台主机运行Server.cpp

一台主机运行Client.cpp

两当台主机处于同一网段下,便可以进行通信

(在不考虑配置路由的情况下,两台主机只有处于同一网段的情况下才能进行通信)

同一网段的概念:同一网段指的是 IP地址 和 子网掩码 相与得到相同的网络地址。想在同一网段,必须做到网络标识相同。

当子网掩码一致的情况下,只需要看 IP地址,如果 IP地址 只有后两位不同,其他位置相同,便可以断定其一定处于同一网段

例如

子网掩码: 255.255.255.0

IP地址: 192.168.43.xx

具体计算的方法如下
以我自己电脑上的IP地址和子网掩码做例子
IP地址: 192.168.43.42
子网掩码: 255.255.255.0

1.首先将I P地址 和 子网掩码 转换为二进制
IP地址 转换为二进制为:

1
11000000‬ ‭10101000‬ ‭00101011‬ ‭00101010

子网掩码 转换为二进制为:

1
11111111‬ ‭11111111‬ ‭1111111100000000

2.然后将转换为二进制的ip地址和子网掩码做与运算就可以算出网络标识

1
2
3
4
5
6
7
ip地址:    11000000‬  ‭10101000‬  ‭00101011‬ ‭00101010
子网掩码: 11111111 111111111111111100000000
网络标识: 11000000‬ ‭10101000‬ ‭00101011 00000000


计算出来的网络标识为:
11000000‬ ‭10101000‬ ‭00101011 00000000

可以在命令行使用 ping xx.xx.xx.xx 进行测试,当得到以下信息时

image-20240922111142211

代表着两台主机是可以进行通信的

注意:需要关掉两台电脑的防火墙,否则会被防火墙拦截请求

二、UDP

1.UDP原理

UDP是一种无连接的协议,它允许数据包立即发送,无需建立和断开连接。UDP的特点包括:

  • 快速传输:由于UDP的头部开销较小,数据包传输速度较快,适用于需要实时传输的应用。

  • 无可靠性:UDP不保证数据包的可靠性传输,不保证数据包的顺序到达,也不保证数据包不会丢失。

  • 广播和多播:UDP支持广播和多播,可以将数据包发送到多个接收者。

在UDP通信中,服务端通常不需要提前知道客户端的地址信息。这是因为UDP是一种面向无连接的协议,它不维护连接状态,也不需要在通信之前建立连接。客户端可以直接向服务器发送数据包,而服务器可以从接收到的数据包中提取客户端的地址信息。

当服务端收到UDP数据包时,可以从数据包中解析出客户端的地址信息(IP地址和端口号),这样服务端就能够知道客户端的地址。服务端可以使用这些信息来向客户端发送响应,如果需要的话。在UDP通信中,客户端和服务端之间的通信是相对简单和直接的,因为不需要维护连接状态,也没有复杂的握手过程。

image-20240922120115500

UDP程序中的发送/接受信息的函数是:

image-20240922115047016

image-20240922115104572

2.UDP服务端程序设计

注意:UDP程序同样是需要 Init.h 文件的,并且文件的内容是一致的

与TCP通信一致

UDP的服务端首先创建一个socket对象( sListen

然后使用 bind 函数 将以下的地址信息绑定到sListen

1
2
3
4
sockaddr_in sin;
sin.sin_family = AF_INET;//设置地址族为IPv4
sin.sin_port = htons(4567);//设置端口号为4567,使用网络字节顺序
sin.sin_addr.S_un.S_addr = INADDR_ANY;//将IP地址设置为任意本地地址

服务端不断监听来自其他程序的发送到此端口上的连接请求信息,只要接收到了连接请求,便可以进行通信

服务端是不需要知道客户端的地址信息的,服务端只需要不断监听连接请求

客户端和服务端运行的时候需要进行一次暂停操作,由于是面向无连接的通信,所以可能存服务端或者客户端未运行,而消息却已发送出去了,导致消息的丢失

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "Init.h"
#include <stdio.h>
CInitSock initSock;
#include<iostream>
#include<stdlib.h>

int main()
{
printf("***********服务端 Server *****************/n");
// 创建套节字
SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s == INVALID_SOCKET)
{
printf("Failed socket() /n");
return 0;
}

// 填充sockaddr_in结构(服务端端口号为4567)
sockaddr_in sin;
sin.sin_family = AF_INET;//设置地址族为IPv4
sin.sin_port = htons(4567);//设置端口号为4567,使用网络字节顺序
sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//将IP地址设置为本地地址

//绑定套接字到服务器地址
if (::bind(s, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("Failed bind() /n");
return 0;
}

sockaddr_in addr;

//暂停一下 等到客户端运行完毕 否则发送的信息会找不到
system("pause");

// 接收连接
char Text[1024];
int Len = sizeof(addr);
while (TRUE)
{
int nRecv = ::recvfrom(s, Text, 1024, 0, (sockaddr*)&addr, &Len);
if (nRecv > 0)
{
Text[nRecv] = '/0';
printf(" 接收到数据(%s : %d):%s", ::inet_ntoa(addr.sin_addr), addr.sin_port, Text);
break;
}
}

//服务端向客户端发送信息
char szText[] = "你好!我是服务端!欢迎对话 /n";
::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));
printf("发送成功");

// 接收数据
char buff[1024];
int nLen = sizeof(addr);
while (TRUE)
{
int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&addr, &nLen);
if (nRecv > 0)
{
buff[nRecv] = '/0';
printf(" 接收到数据(%s : %d):%s", ::inet_ntoa(addr.sin_addr), addr.sin_port, buff);
break;
}
}
::closesocket(s);
}

3.UDP客户端程序设计

注意:UDP程序同样是需要 Init.h 文件的,并且文件的内容是一致的

在UDP通信中,客户端需要指定要连接的服务器的IP地址和端口号

1
2
3
4
5
// 填写远程服务器程序地址信息
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4567);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

客户端和服务端运行的时候需要进行一次暂停操作,由于是面向无连接的通信,所以可能存服务端或者客户端未运行,而消息却已发送出去了,导致消息的丢失

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "Init.h"
#include <stdio.h>
#include<iostream>
using namespace std;
CInitSock initSock;
#include<stdlib.h>


int main()
{
printf("***********客户端 Client *****************/n");
// 创建套节字
SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s == INVALID_SOCKET)
{
printf("Failed socket() %d /n", ::WSAGetLastError());
return 0;
}

// 填充sockaddr_in结构(客户端端口号5678)
sockaddr_in sin;
sin.sin_family = AF_INET;//设置地址族为IPv4
sin.sin_port = htons(5678);//设置端口号为5678,使用网络字节顺序
sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//将IP地址设置为本地地址

//绑定套接字到客户端地址
if (::bind(s, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("Failed bind() /n");
return 0;
}

// 填写远程服务器程序地址信息
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4567);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

system("pause");
// 发送数据
char Text[] = "请求连接 /n";
::sendto(s, Text, strlen(Text), 0, (sockaddr*)&addr, sizeof(addr));


//接受数据
char buff[1024];
int nLen = sizeof(addr);
while (TRUE)
{
int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&addr, &nLen);
if (nRecv > 0)
{
buff[nRecv] = '/0';
printf(" 接收到数据(%s : %d):%s", ::inet_ntoa(addr.sin_addr), addr.sin_port, buff);
break;
}
}

// 发送数据
char szText[] = "我是客户端,谢谢! /n";
::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));


::closesocket(s);
return 0;
}

文章作者: Yolo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yolo !
评论
  目录