深入理解C/C++套接字编程:从基础到实践
前言
网络编程是现代软件开发中不可或缺的技能,而套接字(Socket)编程则是网络通信的基石。无论是在Windows还是Linux平台上,Socket编程的核心思想都是相通的。本文将深入探讨C/C++语言下的Socket编程,从基础概念到实际应用,通过丰富的代码示例帮助读者全面掌握这一重要技术。
套接字编程看似复杂,实则遵循着清晰的模式。无论是TCP还是UDP协议,无论是客户端还是服务器端,其基本流程都有章可循。本文将通过逐步分解的方式,让读者理解网络通信的每一个环节,并能够编写出自己的网络程序。
需要特别说明的是:为保持示例的简洁性和易理解性,本文中的代码未引入多线程机制,所有的例子都是单线程阻塞式的。在实际生产环境中,根据需求可能需要考虑多线程、非阻塞I/O或异步I/O等技术。此外,所有示例都运行在Windows平台,但代码的核心逻辑在Linux平台上同样适用,只需进行少量修改(主要是头文件和库函数的变化)。
调试助手推荐
在学习网络编程时,使用一个网络调试助手可以极大地提高学习效率。通过调试助手,我们可以直观地查看网络数据、模拟各种网络场景,从而更好地理解网络通信的原理。
1. 野人家园的NetAssist网络调试助手
这是一个功能全面的网络调试工具,支持TCP/UDP协议,既可以作为客户端也可以作为服务器,非常适合初学者使用。
下载地址 :NetAssist网络调试助手-软件工具-野人家园
2. 野火的多功能调试助手
野火调试助手不仅支持网络调试,还支持串口、CAN总线等多种通信方式,界面友好,功能强大。
下载地址 :野火多功能调试助手上位机 --- 野火产品资料下载中心
3. 本人的网络助手
如果你对Qt感兴趣,也可以尝试我基于Qt开发的网络助手。这个工具完全开源,你可以查看源代码,了解其实现原理。
下载地址 :Qt网络助手_网络助手发送集成版-CSDN博客
GitHub仓库 :Network_Assistant: Qt自制网络助手
基础知识
在深入套接字编程之前,我们需要先理解一些基础概念。这些概念是理解网络编程的基石,掌握它们将帮助我们更好地理解后续的代码示例。
大小端(Endianness)
大小端是计算机系统中一个重要的概念,特别是在跨平台网络通信中。不同的处理器架构采用不同的字节序,这导致了数据在内存中的存储顺序不同。
说明
大端序(Big-Endian)
- 高位字节存储在低地址,低位字节存储在高地址
- 符合人类的阅读习惯(从左到右 = 从高到低)
- 以十六进制数0x01234567为例(假设起始地址为0x1000):
| 内存地址 | 0x1000 | 0x1001 | 0x1002 | 0x1003 |
|---|---|---|---|---|
| 存储内容 | 01 | 23 | 45 | 67 |
重要提示:🌐 网络字节序(Network Byte Order)就是大端序!所有TCP/IP协议规定:IP地址、端口号等多字节字段必须以大端序传输。
小端序(Little-Endian)
- 低位字节存储在低地址,高位字节存储在高地址
- 绝大多数PC和手机CPU都采用小端序
- 同样以0x01234567为例:
| 内存地址 | 0x1000 | 0x1001 | 0x1002 | 0x1003 |
|---|---|---|---|---|
| 存储内容 | 67 | 45 | 23 | 01 |
注意:💻 主机字节序(Host Byte Order)在大多数PC上是小端序。
小端序之所以被广泛采用,是因为它在CPU运算中更为方便。对于加法运算,从最低位开始计算更为自然,这与小端序的存储方式一致。
相关函数
在网络编程中,我们需要在主机字节序和网络字节序之间进行转换。以下是一组重要的转换函数:
htons() ------ host to network short(16位)
c
uint16_t htons(uint16_t hostshort);
- 作用:将16位无符号整数从主机字节序转为网络字节序
- 参数:hostshort ------ 主机字节序的16位值
- 返回值:网络字节序的16位值
ntohs() ------ network to host short(16位)
c
uint16_t ntohs(uint16_t netshort);
- 作用:将16位无符号整数从网络字节序转为主机字节序
- 参数:netshort ------ 网络字节序的16位值
- 返回值:主机字节序的16位值
htonl() ------ host to network long(32位)
c
uint32_t htonl(uint32_t hostlong);
- 作用:将32位无符号整数从主机字节序转为网络字节序
- 参数:hostlong ------ 主机字节序的32位值
- 返回值:网络字节序的32位值
ntohl() ------ network to host long(32位)
c
uint32_t ntohl(uint32_t netlong);
- 作用:将32位无符号整数从网络字节序转为主机字节序
- 参数:netlong ------ 网络字节序的32位值(如从socket接收的协议字段)
- 返回值:主机字节序的32位值
IP地址简介
IP地址是网络中设备的唯一标识符。了解IP地址的分类和表示方法是网络编程的基础。
IPv4(Internet Protocol version 4)
-
位数:32位(4字节)
-
人类可读格式:点分十进制(Dotted Decimal Notation)
-
将32位地址分为4段,每段8位(1字节),转换为十进制后用点
.分隔:11000000.10101000.00000001.00001010 → 192.168.1.10 -
地址空间:约43亿个(2³² ≈ 4.3×10⁹)
-
特殊地址:
127.0.0.1:本地回环地址(loopback),代表本机0.0.0.0:表示"任意IP"或"未指定地址",常用于服务器绑定255.255.255.255:广播地址
IPv6(Internet Protocol version 6)
- 位数:128位(16字节)
- 人类可读格式:冒号分隔的十六进制(Hexadecimal Colon Notation)
- 将128位地址分为8段,每段16位(2字节),用十六进制表示,段间用冒号
:分隔 - 示例:
2001:0db8:85a3:0000:0000:8a2e:0370:7334 - 可简写为:
2001:db8:85a3::8a2e:370:7334(连续的0可用::代替,但只能用一次) - 特殊地址示例:
::1:IPv6的本地回环地址:::表示"任意IP"或"未指定地址"
私有IP地址
以下范围内的IP地址是私有IP地址(Private IP),只能在局域网内部使用:
10.0.0.0 -- 10.255.255.255172.16.0.0 -- 172.31.255.255192.168.0.0 -- 192.168.255.255
互联网上的其他设备无法直接访问私有IP地址,需要通过NAT(网络地址转换)技术才能与外部通信。
查看本机IP地址(Windows)
可以通过以下方式查看本机IP地址:
- 从网络和共享中心查看:直接在Windows设置中查看网络状态
- 从命令行查看 :使用
ipconfig命令
示例:
cmd
Microsoft Windows [Version 10.0.19045.6466]
(c) Microsoft Corporation. All rights reserved.
C:\Users\Cai>ipconfig
Windows IP Configuration
Wireless LAN adapter WLAN:
Connection-specific DNS Suffix . :
Link-local IPv6 Address . . . . . : fe80::50e:99e5:e69c:d797%14
IPv4 Address. . . . . . . . . . . : 10.53.1.148
Subnet Mask . . . . . . . . . . . : 255.255.240.0
Default Gateway . . . . . . . . . : 10.53.0.1
在上面的示例中,我使用的是WiFi连接,IPv4地址是10.53.1.148,这是一个私有IP地址,由路由器分配。当我的设备需要访问外部网络时,数据包会先到达默认网关(路由器),然后由路由器转发到外部网络。
相关函数简介
inet_pton()函数:字符串IP → 二进制格式
c
int inet_pton(int af, const char* src, void* dst);
- 作用:将可读的IP字符串转换为网络协议所需的二进制格式
- 参数:
af(Address Family):地址族,AF_INET(IPv4)或AF_INET6(IPv6)src:指向IP字符串的指针dst:指向目标缓冲区的指针
- 返回值:
1:成功0:输入不是有效IP地址-1:af不支持
inet_ntop()函数:二进制格式 → 字符串IP
c
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
- 作用:将网络字节序的二进制IP地址转换可读的IP字符串,并存入用户提供的缓冲区
- 参数:
af(Address Family):地址族src:指向二进制IP地址的指针dst:指向目标字符数组的指针,用于存放转换后的字符串size:dst缓冲区的大小(以字节为单位)
- 返回值:
- 成功:返回转换后的字符串地址(即dst指针)
- 失败:返回nullptr
端口(Port)简介
端口是设备中的逻辑编号,用于区分同一设备上不同的网络应用程序或服务。
- 位数:16位无符号整数(0-65535)
- 作用:在一台设备上同时运行多个网络程序时,端口用于确保数据被正确投递给目标程序
- 常用端口:
- 0-1023:知名端口(Well-known ports),通常被系统服务占用
- 1024-49151:注册端口(Registered ports),可供用户程序使用
- 49152-65535:动态/私有端口(Dynamic/Private ports)
注意:在进行测试时,通常选择5000以上的端口号,避免与系统服务冲突。
套接字(Socket)简介
什么是套接字(Socket)
套接字是操作系统提供的一种通信端点(endpoint)抽象,用于在两个网络程序之间进行数据交换。它屏蔽了底层网络协议(如TCP/IP、UDP/IP)的复杂性,让程序员只需关注"发送"和"接收"数据,而无需关心数据如何穿越路由器、如何分片重组等细节。
可以将套接字理解为"网络通信的插座"------一端插在你的程序里,另一端通过网络连接到对方程序。
相关结构体、函数说明
SOCKET类型本质
c
typedef UINT_PTR SOCKET;
- 本质:是一个无符号整数类型
- 作用:作为套接字的句柄(handle),用于标识一个网络连接或监听端点
socket()函数:创建套接字
c
SOCKET socket(int af, int type, int protocol);
- 作用:创建一个新的通信端点(套接字)
- 参数:
af(Address Family):地址族,AF_INET(IPv4)或AF_INET6(IPv6)type(Socket Type):通信方式,SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)protocol:协议(通常设为0,表示自动选择)
- 返回值:
- 成功:返回一个有效的SOCKET值
- 失败:返回
INVALID_SOCKET
closesocket()函数:关闭套接字
c
int closesocket(SOCKET s);
- 作用:关闭一个套接字,释放相关资源
- 参数:
s:要关闭的套接字
- 返回值:
0:成功关闭-1(SOCKET_ERROR):关闭失败
sockaddr_in结构体(IPv4地址)
c
struct sockaddr_in {
short sin_family; // 地址族,必须为AF_INET
unsigned short sin_port; // 端口号(网络字节序!)
struct in_addr sin_addr; // IPv4地址(网络字节序!)
char sin_zero[8]; // 填充字段,必须置0
};
struct in_addr {
uint32_t s_addr; // 实际存储IP的4字节(如0x7F000001=127.0.0.1)
};
- 用途:描述一个IPv4网络地址(IP+端口)
- 初始化建议:
sockaddr_in addr = {};// 零初始化,确保sin_zero全0
bind()函数:将套接字绑定到本地地址和端口
c
int bind(SOCKET s, const struct sockaddr* addr, socklen_t namelen);
- 作用:将套接字关联到本机的一个IP地址和端口号,使其具备"身份标识"
- 参数:
s:由socket()创建的套接字addr:指向地址结构体的指针(如sockaddr_in)namelen:地址结构体的实际大小
- 返回值:
0:绑定成功-1(或SOCKET_ERROR):失败
注意事项:
- 服务器必须调用
bind()来指定监听的IP和端口 - 客户端通常不需要显式
bind(),操作系统会在connect()时自动分配一个临时端口和本地IP - 在同一个协议(TCP/UDP)下,任意两个套接字不能同时绑定到完全相同的(IP地址, 端口号)组合
TCP(传输控制协议)
简介
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠、有序、基于字节流的传输层协议。它是互联网上绝大多数关键应用(如网页浏览、文件传输、电子邮件)的基础。
TCP的主要特点:
- 面向连接:通信前必须先建立连接
- 可靠传输:通过确认机制、重传机制保证数据不丢失、不重复
- 有序传输:数据按发送顺序到达接收方
- 流量控制:防止发送方发送过快导致接收方缓冲区溢出
- 拥塞控制:根据网络状况动态调整发送速率
TCP严格区分客户端与服务器:
- 服务器(Server):被动等待连接请求
- 客户端(Client):主动发起连接请求
三次握手(Three-Way Handshake)------建立连接
TCP在传输数据前,必须通过三次握手建立可靠连接:
客户端 服务器
| SYN=1, seq=x |
| -------------------------> | (1) 客户端请求连接
| SYN=1, ACK=1, |
| seq=y, ack=x+1 |
| <------------------------- | (2) 服务器确认并回应
| ACK=1, ack=y+1 |
| -------------------------> | (3) 客户端确认
| |
| ← 连接已建立 → |
四次挥手(Four-Way Wavehand)------关闭连接
由于TCP连接是全双工(双方可同时收发),关闭时需分别关闭两个方向,因此通常需要四次交互:
主动关闭方(如客户端) 被动关闭方(服务器)
| FIN=1, seq=u |
| -------------------------> | (1) 客户端:我数据发完了
| ACK=1, ack=u+1 |
| <------------------------- | (2) 服务器:收到,但我还可能有数据发你
| FIN=1, seq=v |
| <------------------------- | (3) 服务器:我也发完了
| ACK=1, ack=v+1 |
| -------------------------> | (4) 客户端:收到,再见
相关函数
connect()函数:发起TCP连接
c
int connect(SOCKET s, const struct sockaddr* name, int namelen);
- 作用:主动向服务器发起TCP三次握手
- 参数:
s:由socket()创建的客户端套接字name:指向服务器地址结构的指针namelen:地址结构大小
- 返回值:
0:连接成功SOCKET_ERROR(即-1):失败
- 注意:默认为阻塞模式,会阻塞线程直到连接成功或失败!
listen()函数:将套接字设为监听状态
c
int listen(SOCKET s, int backlog);
- 作用:将一个已绑定的TCP套接字转为被动监听模式
- 参数:
s:已绑定(bind)的TCP套接字backlog:等待连接队列的最大长度
- 返回值:
0:成功进入监听状态-1(或SOCKET_ERROR):失败
- 注意 :仅用于服务器端,必须在
accept()之前调用!
accept()函数:接受一个客户端连接
c
SOCKET accept(SOCKET s, struct sockaddr* addr, socklen_t* addrlen);
- 作用:从监听套接字的连接请求队列中取出一个已完成三次握手的连接
- 参数:
s:处于监听状态的服务器套接字addr:可选,用于获取客户端地址信息addrlen:传入/传出参数,地址结构体大小
- 返回值:
- 成功:返回一个全新的已连接套接字描述符
- 失败:返回
INVALID_SOCKET
- 注意:默认为阻塞模式!当没有新连接时会阻塞当前线程!
recv()函数:接收TCP数据
c
int recv(SOCKET s, char* buf, int len, int flags);
- 作用:从已连接的TCP套接字接收数据
- 参数:
s:已连接的套接字buf:接收缓冲区len:缓冲区最大长度flags:通常为0
- 返回值:
> 0:实际接收到的字节数0:对端正常关闭连接(收到FIN)-1(SOCKET_ERROR):发生错误
- 注意:默认为阻塞模式!在没有数据可读时会阻塞当前线程!
send()函数:发送TCP数据
c
int send(SOCKET s, const char* buf, int len, int flags);
- 作用:通过已连接的TCP套接字向对端发送数据
- 参数:
s:已连接的套接字buf:指向要发送的数据缓冲区len:要发送的字节数flags:通常为0
- 返回值:
> 0:成功发送的字节数-1(SOCKET_ERROR):发送失败
TCP客户端程序示例
例1-只能接收数据的TCP客户端
这是一个简单的TCP客户端,它只能接收服务器发送的数据,而不能向服务器发送数据。
cpp
// 只接收服务器数据的TCP客户端(Windows版)
#include <iostream>
#include <string>
using namespace std;
#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main(void)
{
cout << "只能接收服务器数据的TCP客户端" << endl;
// 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
WSADATA wsaData;
int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (wsaResult != 0)
{
cerr << "WSAStartup failed: " << wsaResult << endl;
return 1;
}
// 创建通信的套接字
cout << "创建套接字" << endl;
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET)
{
cerr << "套接字创建失败" << endl;
cerr << "错误码 " << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
// 由用户输入目标的IP地址和端口
cout << "\n请输入服务器IP地址" << endl;
char targetIP[128] = { 0 };
cin >> targetIP;
cout << "请输入目标端口" << endl;
uint16_t targetPort = UINT16_MAX;
cin >> targetPort;
// 要连接的服务器IP端口
sockaddr_in targetAddr = { 0 };
targetAddr.sin_family = AF_INET; // 设置地址簇
targetAddr.sin_port = htons(targetPort); // 设置目标的端口
inet_pton(AF_INET, targetIP, &targetAddr.sin_addr.s_addr); // 将字符串IP转为二进制格式
// 发起TCP连接,主动向服务器发起TCP三次握手
cout << "\n\n发起TCP连接" << endl;
// 默认为阻塞模式,调用后线程被挂起!!!直到连接成功或者失败
int result = connect(clientSocket, (sockaddr*)&targetAddr, sizeof(targetAddr));
if (result == SOCKET_ERROR)
{
cerr << "连接失败 " << endl;
cerr << "错误码 " << WSAGetLastError() << endl;
return 1;
}
cout << "\nTCP连接成功开始接收数据\n\n" << endl;
// 接收数据
while (1)
{
char receiveBuffer[1024] = { 0 }; // 缓冲区
// 接收数据,默认没有数据可读时阻塞当前线程!!!!
int bytesReceived = recv(clientSocket, receiveBuffer, sizeof(receiveBuffer), 0);
if (bytesReceived > 0)
{
cout << "接收到了 " << bytesReceived << " 字节数据:" << endl;
cout << receiveBuffer << endl;
}
else if (bytesReceived == 0)
{
cout << "服务器断开了连接" << endl;
break;
}
else
{
cerr << "出现错误" << endl;
cerr << "错误码 " << WSAGetLastError() << endl;
break;
}
}
closesocket(clientSocket); // 关闭描述符
WSACleanup(); // Windows下需要清理
cout << "\n\n客户端程序停止运行" << endl;
return 0;
}
使用说明:
- 编译并运行该程序
- 输入服务器的IP地址(如127.0.0.1)和端口号(如8080)
- 程序会尝试连接到指定的服务器
- 连接成功后,程序会等待接收服务器发送的数据
- 当服务器断开连接时,程序会退出
注意事项:
- 这是一个阻塞式的客户端,在等待连接和接收数据时会阻塞当前线程
- 程序没有错误恢复机制,一旦出错就会退出
- 只能接收数据,不能发送数据
例2-只能发送数据的TCP客户端
这个TCP客户端只能向服务器发送数据,而不能接收服务器发送的数据。
cpp
// 只能向服务器发送数据的TCP客户端(Windows版)
#include <iostream>
#include <string>
using namespace std;
#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main(void)
{
cout << "只能向服务器发送数据的TCP客户端" << endl;
// 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
WSADATA wsaData;
int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (wsaResult != 0)
{
cerr << "WSAStartup failed: " << wsaResult << endl;
return 1;
}
// 创建通信的套接字
cout << "创建套接字" << endl;
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET)
{
cerr << "套接字创建失败 错误码 " << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
// 由用户输入目标的IP地址和端口
cout << "\n请输入目标服务器IP地址" << endl;
char targetIP[128] = { 0 };
cin >> targetIP;
cout << "请输入目标端口" << endl;
uint16_t targetPort = UINT16_MAX;
cin >> targetPort;
// 连接服务器IP端口
sockaddr_in targetAddr = { 0 };
targetAddr.sin_family = AF_INET; // 设置地址簇
targetAddr.sin_port = htons(targetPort); // 设置目标的端口
inet_pton(AF_INET, targetIP, &targetAddr.sin_addr.s_addr); // 将字符串IP转为二进制格式
// 发起TCP连接,主动向服务器发起TCP三次握手
// 默认为阻塞模式,调用后线程被挂起!!!直到连接成功或者失败
cout << "\n\n发起TCP连接" << endl;
int result = connect(clientSocket, (sockaddr*)&targetAddr, sizeof(targetAddr));
if (result == SOCKET_ERROR)
{
cerr << "连接失败 错误码 " << WSAGetLastError() << endl;
return 1;
}
cout << "TCP连接成功" << endl;
cout << "\n可以发送数据了 输入 __QUIT__ 停止发送\n" << endl;
// 发送数据
while (1)
{
cout << "\n请输入字符串" << endl;
string userString;
cin >> userString;
if (userString.empty()) continue;
if (userString == "__QUIT__")
{
break;
}
// 发送数据
int sendResult = send(clientSocket, userString.c_str(), userString.size(), 0);
if (sendResult == SOCKET_ERROR)
{
cout << "发送失败 错误码: " << WSAGetLastError() << endl;
continue;
}
else
{
cout << "成功发送 " << sendResult << "字节数据" << endl;
}
}
closesocket(clientSocket); // 关闭描述符
WSACleanup(); // Windows下需要清理
cout << "\n\n客户端程序停止运行" << endl;
return 0;
}
使用说明:
- 编译并运行该程序
- 输入服务器的IP地址和端口号
- 程序会尝试连接到指定的服务器
- 连接成功后,可以输入要发送的字符串
- 输入
__QUIT__可以退出程序
注意事项:
- 这个程序只能发送数据,不能接收服务器返回的数据
- 发送的数据是字符串格式
- 每次发送后不会等待服务器的响应
TCP服务器程序示例
例1-只能接收数据的TCP服务器
这是一个简单的TCP服务器,它只能接收一个客户端连接,并且只能接收客户端发送的数据,不能向客户端发送数据。
cpp
// 只能接收一个客户端连接且只能接收客户端数据的TCP服务器(Windows版)
#include <iostream>
#include <string>
using namespace std;
#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main(void)
{
cout << "只能接收一个客户端连接且只能接收客户端数据的TCP服务器\n" << endl;
// 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
WSADATA wsaData;
int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (wsaResult != 0)
{
cerr << "WSAStartup failed: " << wsaResult << endl;
return 1;
}
// 创建监听的套接字,专门负责监听有没有客户端请求连接
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET)
{
cerr << "监听套接字创建失败" << endl;
cerr << "错误码 " << WSAGetLastError() << endl;
return 1;
}
// 由用户输入本机的IP地址和要监听的端口
// 0.0.0.0表示本机任意的IP地址
cout << "\n请输入要绑定的本机IP地址" << endl;
char localIP[128] = { 0 };
cin >> localIP;
cout << "请输入要监听的端口" << endl;
uint16_t port = UINT16_MAX;
cin >> port;
// 设置服务器地址结构
sockaddr_in serverAddr = { 0 };
serverAddr.sin_family = AF_INET; // 设置地址簇
serverAddr.sin_port = htons(port); // 设置要监听的端口
// 将字符串IP转为二进制格式
inet_pton(AF_INET, localIP, &serverAddr.sin_addr.s_addr);
// 绑定
cout << "\n\n进行绑定" << endl;
if (bind(listenSocket, (sockaddr*)(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR)
{
cerr << "绑定失败,错误码: " << WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
cout << "绑定成功\n" << endl;
// 设置监听
cout << "设置监听" << endl;
if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
{
cerr << "监听设置失败 错误码 " << WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
cout << "监听设置成功,等待客户端连接\n" << endl;
sockaddr_in clientAddr = { 0 };
int clientAddr_Len = sizeof(clientAddr);
// 接受客户端的连接,默认阻塞线程等待客户端!!!
SOCKET clientSocket = accept(listenSocket, (sockaddr*)(&clientAddr), &clientAddr_Len);
if (clientSocket == INVALID_SOCKET)
{
cerr << "接受连接失败 错误码 " << WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
else
{
char clientIP[INET_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET, &clientAddr, clientIP, sizeof(clientIP));
uint16_t clientPort = ntohs(clientAddr.sin_port);
cout << "接受客户端的连接" << endl;
cout << "客户端IP " << clientIP << endl;
cout << "客户端端口 " << clientPort << endl;
}
cout << "\nTCP连接成功开始接收数据\n\n" << endl;
while (1)
{
char receiveBuffer[1024] = { 0 }; // 缓冲区
// 接收数据,默认没有数据可读时阻塞当前线程!!!!
int bytesReceived = recv(clientSocket, receiveBuffer, sizeof(receiveBuffer), 0);
if (bytesReceived > 0)
{
cout << "接收到了 " << bytesReceived << " 字节数据:" << endl;
cout << receiveBuffer << endl;
}
else if (bytesReceived == 0)
{
cout << "客户端断开了连接" << endl;
closesocket(clientSocket); // 关闭客户端套接字
break;
}
else
{
cerr << "出现错误" << endl;
cerr << "错误码 " << WSAGetLastError() << endl;
break;
}
}
closesocket(listenSocket); // 关闭监听套接字
WSACleanup(); // Windows下需要清理
cout << "\n\n服务器程序停止运行" << endl;
return 0;
}
使用说明:
- 编译并运行该程序
- 输入要绑定的本地IP地址(如127.0.0.1或0.0.0.0)和端口号(如8080)
- 程序会开始监听指定端口的连接请求
- 当有客户端连接时,程序会接受连接并显示客户端信息
- 连接建立后,程序会等待接收客户端发送的数据
- 当客户端断开连接时,程序会退出
注意事项:
- 这个服务器只能处理一个客户端连接
- 服务器是阻塞式的,在等待连接和接收数据时会阻塞当前线程
- 服务器只能接收数据,不能向客户端发送数据
例2-只能发送数据的TCP服务器
这个TCP服务器只能接收一个客户端连接,并且只能向客户端发送数据,不能接收客户端发送的数据。
cpp
// 只能接收一个客户端连接且只能向客户端发送数据的TCP服务器(Windows版)
#include <iostream>
#include <string>
using namespace std;
#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main(void)
{
cout << "只能接收一个客户端连接且只能向客户端发送数据的TCP服务器\n" << endl;
// 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
WSADATA wsaData;
int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (wsaResult != 0)
{
cerr << "WSAStartup failed: " << wsaResult << endl;
return 1;
}
// 创建监听的套接字,专门负责监听有没有客户端连接
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET)
{
cerr << "监听套接字创建失败" << endl;
cerr << "错误码 " << WSAGetLastError() << endl;
return 1;
}
// 由用户输入本机的IP地址和要监听的端口
// 0.0.0.0表示本机任意的IP地址
cout << "\n请输入要绑定的本机IP地址" << endl;
char localIP[128] = { 0 };
cin >> localIP;
cout << "请输入要监听的端口" << endl;
uint16_t port = UINT16_MAX;
cin >> port;
// 设置服务器地址结构
sockaddr_in serverAddr = { 0 };
serverAddr.sin_family = AF_INET; // 设置地址簇
serverAddr.sin_port = htons(port); // 设置要监听的端口
// 将字符串IP转为二进制格式
inet_pton(AF_INET, localIP, &serverAddr.sin_addr.s_addr);
// 绑定
cout << "\n\n进行绑定" << endl;
if (bind(listenSocket, (sockaddr*)(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR)
{
cerr << "绑定失败,错误码: " << WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
cout << "绑定成功\n" << endl;
// 设置监听
cout << "设置监听" << endl;
if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
{
cerr << "监听设置失败 错误码 " << WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
cout << "开始监听\n" << endl;
sockaddr_in clientAddr = { 0 };
int clientAddr_Len = sizeof(clientAddr);
// 接受客户端的连接,默认阻塞线程等待客户端!!!
cout << "等待客户端连接" << endl;
SOCKET clientSocket = accept(listenSocket, nullptr, nullptr);
if (clientSocket == INVALID_SOCKET)
{
cerr << "接受连接失败 错误码 " << WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
else
{
char clientIP[INET_ADDRSTRLEN] = { 0 };
inet_ntop(AF_INET, &clientAddr, clientIP, sizeof(clientIP));
uint16_t clientPort = ntohs(clientAddr.sin_port);
cout << "接受客户端的连接" << endl;
cout << "客户端IP " << clientIP << endl;
cout << "客户端端口 " << clientPort << endl;
}
cout << "\nTCP连接成功开始发送数据" << endl;
cout << "输入 __QUIT__ 停止发送\n" << endl;
while (1)
{
cout << "\n请输入字符串" << endl;
string userString;
cin >> userString;
if (userString.empty()) continue;
if (userString == "__QUIT__")
{
break;
}
// 发送数据
int sendResult = send(clientSocket, userString.c_str(), userString.size(), 0);
if (sendResult == SOCKET_ERROR)
{
cout << "发送失败 错误码: " << WSAGetLastError() << endl;
continue;
}
else
{
cout << "成功发送 " << sendResult << "字节" << endl;
}
}
closesocket(clientSocket); // 关闭客户端套接字
closesocket(listenSocket); // 关闭监听套接字
WSACleanup(); // Windows下需要清理
cout << "\n\n服务器程序停止运行" << endl;
return 0;
}
使用说明:
- 编译并运行该程序
- 输入要绑定的本地IP地址和端口号
- 程序会开始监听指定端口的连接请求
- 当有客户端连接时,程序会接受连接
- 连接建立后,可以输入要发送给客户端的字符串
- 输入
__QUIT__可以停止发送并退出程序
注意事项:
- 这个服务器只能处理一个客户端连接
- 服务器只能发送数据,不能接收客户端发送的数据
- 发送的数据是字符串格式
UDP(用户数据报协议)
简介
UDP(User Datagram Protocol,用户数据报协议)是一种无连接、不可靠、基于消息的传输层协议。它不保证数据一定到达、也不保证顺序,但简单、高效、低延迟。
UDP的主要特点:
- 无连接:通信前不需要建立连接
- 不可靠:不保证数据到达,不保证顺序
- 高效:协议头开销小,传输效率高
- 基于数据报:每次发送一个完整、独立的数据包
与TCP不同,UDP是对等(peer-to-peer)的,没有客户端/服务器之分。程序的角色由应用逻辑定义,而非协议强制。
相关函数
recvfrom()函数:接收一个UDP数据报
c
int recvfrom(SOCKET s, char* buf, int len, int flags,
struct sockaddr* from, int* fromlen);
- 作用:从套接字接收一个UDP数据报,并获取发送方的地址信息
- 参数:
s:已创建并绑定的套接字buf:用于存放接收到的数据的缓冲区len:缓冲区的最大容量flags:接收标志(通常设为0)from:指向sockaddr结构体的指针,用于输出发送方的地址fromlen:输入/输出参数,地址结构体大小
- 返回值:
- 成功:返回接收到的字节数
- 失败:返回
SOCKET_ERROR
- 注意 :默认情况下
recvfrom()会阻塞当前线程,直到有数据报到达
sendto()函数:发送一个UDP数据报
c
int sendto(SOCKET s, const char* buf, int len, int flags,
const struct sockaddr* to, int tolen);
- 作用:向指定目标地址发送一个UDP数据报
- 参数:
s:已创建的套接字描述符buf:指向要发送数据的缓冲区len:要发送的字节数flags:发送标志(通常设为0)to:指向目标地址结构体的指针tolen:地址结构体的大小
- 返回值:
- 成功:返回实际发送的字节数
- 失败:返回
SOCKET_ERROR
- 注意 :在UDP中,没有"建立连接"的行为,每次调用
sendto()都需指定目标地址
UDP程序示例
例1-只能发送数据的UDP程序
这个UDP程序只能发送数据,不能接收数据。每次发送数据时都需要指定目标地址。
cpp
// 只能发送数据的UDP程序
#include <iostream>
#include <string>
using namespace std;
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
cout << "只能发送数据的UDP程序\n" << endl;
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
cerr << "WSAStartup失败!\n";
return 1;
}
// 创建UDP套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET)
{
cerr << "创建套接字失败 错误码 " << WSAGetLastError();
WSACleanup();
return 1;
}
cout << "UDP套接字创建成功\n" << endl;
cout << "可以开始发送了\n" << endl;
while (true)
{
string targetIP; // 本次目标IP
uint16_t targetPort; // 本次目标端口
cout << "请输入本次目标IP: " << endl;
cin >> targetIP;
cout << "请输入本次目标端口: " << endl;
cin >> targetPort;
// 设置本次目标地址
sockaddr_in destAddr{};
destAddr.sin_family = AF_INET;
destAddr.sin_port = htons(targetPort);
if (inet_pton(AF_INET, targetIP.c_str(), &destAddr.sin_addr) <= 0) {
cerr << "无效的IP地址\n";
continue;
}
cout << "请输入本次要发送的字符串 输入__QUIT__退出" << endl;
string message;
cin >> message;
if (message == "__QUIT__") break;
// 发送数据(UDP)
int sent = sendto(sock,
message.c_str(), (int)message.size(),
0,
(sockaddr*)&destAddr, sizeof(destAddr));
if (sent == SOCKET_ERROR)
{
cerr << "发送失败,错误码: " << WSAGetLastError() << endl;
}
else
{
cout << "成功发送 " << sent << " 字节数据\n" << endl;
}
}
closesocket(sock);
WSACleanup();
cout << "\n客户端退出。\n";
return 0;
}
使用说明:
- 编译并运行该程序
- 每次发送数据前,需要输入目标IP地址和端口号
- 然后输入要发送的字符串
- 输入
__QUIT__可以退出程序
注意事项:
- 这个程序没有绑定本地端口,操作系统会自动分配一个临时端口
- 每次发送都可以指定不同的目标地址
- 程序不能接收数据,只能发送数据
例2-只能接收数据的UDP程序
这个UDP程序只能接收数据,不能发送数据。它需要绑定到一个本地端口来接收数据。
cpp
// 只能接收数据的UDP程序
#include <iostream>
#include <string>
using namespace std;
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
cout << "只能接收数据的UDP程序\n" << endl;
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
cerr << "WSAStartup失败!\n";
return 1;
}
// 创建UDP套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET)
{
cerr << "创建套接字失败 错误码 " << WSAGetLastError();
WSACleanup();
return 1;
}
cout << "UDP套接字创建成功\n" << endl;
// 由用户输入本机的IP地址和要监听的端口
// 0.0.0.0表示本机任意的IP地址
string localIP; // 本地IP
uint16_t localPort; // 本地端口
cout << "请输入要监听的IP: " << endl;
cin >> localIP;
cout << "请输入要监听的端口: " << endl;
cin >> localPort;
// 需要使用的结构体
sockaddr_in localAddr{};
localAddr.sin_family = AF_INET;
localAddr.sin_port = htons(localPort);
// 将字符串IP转为二进制格式
inet_pton(AF_INET, localIP.c_str(), &(localAddr.sin_addr.s_addr));
// 绑定本地IP端口
if (bind(sock, (sockaddr*)(&localAddr), sizeof(localAddr)) == SOCKET_ERROR)
{
cerr << "绑定失败 错误码 " << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
cout << "绑定成功\n" << endl;
cout << "开始接收数据" << endl;
while (true)
{
char receiveBuffer[1024] = { 0 };
sockaddr_in senderAddr;
int sockaddr_Len = sizeof(senderAddr);
// 接收UDP数据包
// 默认阻塞等待!!!
int bytesReceived = recvfrom(sock,
receiveBuffer, sizeof(receiveBuffer),
0,
(sockaddr*)(&senderAddr), &sockaddr_Len);
if (bytesReceived == SOCKET_ERROR)
{
cerr << "出现错误 错误码 " << WSAGetLastError();
continue;
}
else
{
cout << "收到 " << bytesReceived << " 字节数据" << endl;
cout << receiveBuffer << endl;
char senderIP[INET_ADDRSTRLEN] = {};
inet_ntop(AF_INET, &senderAddr.sin_addr, senderIP, sizeof(senderIP));
uint16_t senderPort = ntohs(senderAddr.sin_port);
cout << "发送方IP " << senderIP << endl;
cout << "发送方端口 " << senderPort << endl;
cout << endl;
}
}
closesocket(sock);
WSACleanup();
cout << "\n客户端退出。\n";
return 0;
}
使用说明:
- 编译并运行该程序
- 输入要绑定的本地IP地址和端口号
- 程序会开始监听指定端口的数据
- 当有数据到达时,程序会显示数据内容和发送方信息
注意事项:
- 这个程序只能接收数据,不能发送数据
- 程序需要绑定到一个本地端口才能接收数据
- 程序是阻塞式的,在等待数据时会阻塞当前线程
后记
本文主要聚焦于套接字编程的基本使用,通过简单的示例展示了TCP和UDP通信的基本原理。这些示例都是单线程阻塞式的,功能相对简单,但它们是理解网络编程的基础。
需要特别指出的是,无论Windows还是Linux,Socket都可以设置为非阻塞模式!通过配合"轮询查询"或者更高效的"事件驱动"机制(如Windows的I/O完成端口或Linux的epoll),单线程也可以实现复杂的网络应用,如一个TCP服务器同时处理多个客户端连接。
在Windows下,设置套接字为非阻塞模式的简单方法如下:
c
// 控制套接字I/O行为
int ioctlsocket(SOCKET s, long cmd, u_long* argp);
另外,需要提醒的是:本教程讲解的Winsock(Windows)以及BSD Socket(Linux/macOS)是操作系统提供的底层网络接口,使用相对繁琐。截至目前(C++23),C++标准库仍未包含网络编程支持。因此,建议这些底层API只用于入门学习。
若需开发实际项目,建议使用成熟的跨平台网络库,如:
- Qt Network:Qt框架的网络模块,简洁易用
- Boost.Asio:功能强大的跨平台网络库
- Poco::Net:全面的C++网络库
这些库以简洁的接口封装了底层细节,大幅提升了开发效率与可靠性。
网络编程是一个深奥而有趣的领域,掌握了基础之后,可以进一步学习多线程编程、异步I/O、网络协议设计等高级主题。希望本文能为你的网络编程学习之旅提供一个良好的起点。
参考资料
-
爱编程的大丙
-
Beej's Guide to Network Programming
- 经典网络编程指南,有中文翻译版
-
UNIX网络编程 卷1:套接字联网API
- W. Richard Stevens的经典著作,网络编程的圣经
-
TCP/IP详解 卷1:协议
- 深入理解TCP/IP协议栈的必备书籍
版权声明:本文中的代码示例可以自由使用、修改和分发,但请保留原作者信息。文中提到的第三方工具和资源,版权归各自所有者所有。
更新日志:
- 2024年1月:初版发布
- 2024年3月:添加UDP示例,修正部分错误
作者 :Cai
联系方式:可通过CSDN博客留言联系
希望这篇超过10000字的详细教程能够帮助你全面理解C/C++套接字编程。如果有任何问题或建议,欢迎留言讨论!