基于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; sin.sin_port = htons(5678); sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
|

暂且不细谈三次握手和四次挥手
用简单通俗的语言来解释就是
三次握手:
①客户端 向 服务端 发送连接请求的消息
②服务端 收到 客户端的请求后 发送一次 ‘收到’ 消息
③客户端 收到 服务端的‘收到’ 消息后 回复一次 ‘确认收到’ 的消息
四次挥手:
①客户端 向 服务端发送 ’请求断开连接的消息‘
②服务端 回复’收到请求断开连接的消息‘
③服务端 将剩下的内容全部收拾完毕后 再次发送 ‘我已经准备好了’ 的消息
④客户端在接收到信息后,便可正常断开连接了
TCP程序中的发送/接受信息的函数是:


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; sin.sin_port = htons(4567); sin.sin_addr.S_un.S_addr = INADDR_ANY;
|
服务端不断监听来自其他程序的发送到此端口上的连接请求信息,只要接收到了连接请求,便可以进行通信
服务端是不需要知道客户端的地址信息的,服务端只需要不断监听连接请求
服务端完整的代码如下:
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"); SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sListen == INVALID_SOCKET) { printf("Failed socket()/n"); return 0; } sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(4567); sin.sin_addr.S_un.S_addr = INADDR_ANY;
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; int nAddrLen = sizeof(remoteAddr); SOCKET sClient; char szText[] = "你好!我是服务端!欢迎对话!/n";
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.sin_family = AF_INET; servAddr.sin_port = htons(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"); SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s == INVALID_SOCKET) { printf("Failed socket()/n"); return 0; } sockaddr_in servAddr; servAddr.sin_family = AF_INET; servAddr.sin_port = htons(4567); servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
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 11111111 00000000
|
2.然后将转换为二进制的ip地址和子网掩码做与运算就可以算出网络标识
1 2 3 4 5 6 7
| ip地址: 11000000 10101000 00101011 00101010 子网掩码: 11111111 11111111 11111111 00000000 网络标识: 11000000 10101000 00101011 00000000 计算出来的网络标识为: 11000000 10101000 00101011 00000000
|
可以在命令行使用 ping xx.xx.xx.xx 进行测试,当得到以下信息时

代表着两台主机是可以进行通信的
注意:需要关掉两台电脑的防火墙,否则会被防火墙拦截请求
二、UDP
1.UDP原理
UDP是一种无连接的协议,它允许数据包立即发送,无需建立和断开连接。UDP的特点包括:
快速传输:由于UDP的头部开销较小,数据包传输速度较快,适用于需要实时传输的应用。
无可靠性:UDP不保证数据包的可靠性传输,不保证数据包的顺序到达,也不保证数据包不会丢失。
广播和多播:UDP支持广播和多播,可以将数据包发送到多个接收者。
在UDP通信中,服务端通常不需要提前知道客户端的地址信息。这是因为UDP是一种面向无连接的协议,它不维护连接状态,也不需要在通信之前建立连接。客户端可以直接向服务器发送数据包,而服务器可以从接收到的数据包中提取客户端的地址信息。
当服务端收到UDP数据包时,可以从数据包中解析出客户端的地址信息(IP地址和端口号),这样服务端就能够知道客户端的地址。服务端可以使用这些信息来向客户端发送响应,如果需要的话。在UDP通信中,客户端和服务端之间的通信是相对简单和直接的,因为不需要维护连接状态,也没有复杂的握手过程。

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


2.UDP服务端程序设计
注意:UDP程序同样是需要 Init.h 文件的,并且文件的内容是一致的
与TCP通信一致
UDP的服务端首先创建一个socket对象( sListen )
然后使用 bind 函数 将以下的地址信息绑定到sListen 上
1 2 3 4
| sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(4567); sin.sin_addr.S_un.S_addr = INADDR_ANY;
|
服务端不断监听来自其他程序的发送到此端口上的连接请求信息,只要接收到了连接请求,便可以进行通信
服务端是不需要知道客户端的地址信息的,服务端只需要不断监听连接请求
客户端和服务端运行的时候需要进行一次暂停操作,由于是面向无连接的通信,所以可能存服务端或者客户端未运行,而消息却已发送出去了,导致消息的丢失
完整代码如下:
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 sin; sin.sin_family = AF_INET; sin.sin_port = htons(4567); sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
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 sin; sin.sin_family = AF_INET; sin.sin_port = htons(5678); sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
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; }
|