深入理解C/C++套接字编程:从基础到实践(超详细)

2026-02-26 11:28:34

深入理解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.255

172.16.0.0 -- 172.31.255.255

192.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

#include

using namespace std;

#include // Windows下使用套接字需要包含

#include

#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

#include

using namespace std;

#include // Windows下使用套接字需要包含

#include

#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

#include

using namespace std;

#include // Windows下使用套接字需要包含

#include

#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

#include

using namespace std;

#include // Windows下使用套接字需要包含

#include

#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

#include

using namespace std;

#include

#include

#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

#include

using namespace std;

#include

#include

#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、网络协议设计等高级主题。希望本文能为你的网络编程学习之旅提供一个良好的起点。

参考资料

爱编程的大丙

图文教程:套接字-Socket | 爱编程的大丙

视频教程:并发网络通信-套接字通信(C/C++ 多线程)_哔哩哔哩_bilibili

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++套接字编程。如果有任何问题或建议,欢迎留言讨论!

为什么国产手机的超大杯全都采用曲面屏设计?看完你就懂了
【老衲說肉】如何養出頂級和牛?