当前位置: 首页 > news >正文

C++ --- Socket套接字的使用

目录

一.什么是Socket套接字?

二.Socket的使用:

前置步骤:

为什么要加入 WSAStartup 和 WSACleanup ?

1.创建Socket:

2.绑定Socket:

3.服务端监听连接请求:

4.服务端接受客户端连接:

5.客户端连接服务端:

6.数据传输:

(1)recv的使用:

 (2)send的使用:

7.关闭Socket:

8.案例分析:

三.TCP协议流程:

四.UDP协议流程:

前置知识:

UDP 的特性: 

UDP的使用:

接收和发送数据的函数:

recvfrom() 函数

sendto() 函数

1.UDP的服务端:

2.UDP的客户端:

 五.前置基础知识总结:

1. 网络基础知识:

2. Socket 的基本概念:

3. Socket 编程中的常用 API:

创建和管理 Socket:

发送和接收数据:

地址结构和网络信息:

4. 常见的 Socket 编程模式:

客户端-服务器模式:

广播和多播:

5. 网络调试和错误处理 :


下面是写博客时参考总结的博客地址:

1.从零开始的C++网络编程-腾讯云开发者社区-腾讯云

2.C++高性能网络编程 | Jack Huang's Blog

3.C/C++网络编程基础知识超详细讲解第一部分(系统性学习day11)-阿里云开发者社区

一.什么是Socket套接字?

Socket 是一种用于计算机之间网络通信的端点。它允许两个程序通过网络交换数据。Socket编程主要用于客户端和服务器的通信,一般可以使用两种协议:

  • TCP (Transmission Control Protocol):可靠、面向连接的协议,适合需要完整传输的数据。TCP 是面向连接的协议,通信前需要通过三次握手(3-way handshake)建立连接。保证数据的可靠性,数据包丢失时会自动重传。通过滑动窗口等技术进行流量控制,避免网络拥塞。TCP 保证数据按顺序到达。
  • UDP (User Datagram Protocol):不可靠、无连接的协议,适合对速度要求高、允许丢包的数据传输。UDP 是无连接的,不需要建立连接,数据可以直接发送。不保证数据的可靠送达,也不保证数据顺序。由于没有建立连接的开销,UDP 相比 TCP 更加高效,适合实时应用。

创建 socket 的时候需要指定 socket 的类型,一般有三种:

  • SOCK_STREAM:面向连接的稳定通信,底层是 TCP 协议,我们会一直使用这个。
  • SOCK_DGRAM:无连接的通信,底层是 UDP 协议,需要上层的协议来保证可靠性。
  • SOCK_RAW:更加灵活的数据控制,能让你指定 IP 头部

想了解前置知识可以跳转五 -->

二.Socket的使用:

 

下面我们基于TCP协议来详细讲述Socket的使用。 

我们在编写C++程序运行后代表的是服务端,浏览器则是客户端:首先创建一个套接字(socket()),绑定(bind())到本地的IP地址和端口,然后进入监听(listen())状态,服务端接受(accept())客户端,客户端连接(connect)服务端。连接(connect)成功后,可以接收(recv)客户端数据和发送(send)数据给客户端。通信完成后,关闭(close())Socket释放资源。

前置步骤:

我们在 Windows 平台上使用 Winsock 编程(即使用 Socket 进行网络通信)时,必须初始化 Winsock 库并在使用结束后清理它。 

#include <iostream>
#include <string.h>
#include <WinSock2.h> // 声明了所有与 Windows 套接字(Socket)相关的函数和数据结构。WSADATA 结构体以及 WSAStartup、WSACleanup 函数也在此头文件中定义。
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {// 初始化 Winsock 库WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {cerr << "WSAStartup failed: " << WSAGetLastError() << endl;return -1;}//socket流程...// 释放Winsock的资源WSACleanup();
}
  • MAKEWORD(2, 2) 表示请求使用版本 2.2 的 Winsock API,&wsaData 是一个指向 WSADATA 结构体的指针,Winsock 会将相关信息存储在这个结构体中。
  • 如果初始化失败,WSAStartup 会返回非零值,我们可以使用 WSAGetLastError() 获取错误代码。

为什么要加入 WSAStartupWSACleanup

Winsock 是 Windows 的网络编程接口,它为 Windows 操作系统提供了网络应用程序接口(API),允许程序通过 TCP/IP 协议族进行通信。在 Windows 中使用 Socket 之前,必须首先调用 WSAStartup 来初始化 Winsock 库。它的作用是让应用程序和 Winsock 层的网络操作系统组件建立联系。WSAStartup 会检查 Winsock 库的版本是否与系统兼容,并加载网络功能所需的组件。每个成功调用 WSAStartup 的应用程序,在程序结束时都需要调用 WSACleanup 来清理 Winsock 使用的资源。它会关闭网络资源并释放内存等,以防止内存泄漏或其他资源问题。 

这个步骤与在其他操作系统(如 Linux)中进行网络编程时的步骤不同,因为 Linux 不需要这样的显式初始化操作。Linux 的网络 API 是直接通过系统调用实现的,使用 socket 和相关函数时不需要先初始化网络库。 

 

1.创建Socket:

要创建一个Socket,需要使用socket()函数。

SOCKET socket(int domain, int type, int protocol);
  • domain:通信协议族,AF_INET用于IPv4,AF_INET6用于IPv6。
  • type:指定Socket类型,SOCK_STREAM用于TCP,SOCK_DGRAM用于UDP。
  • protocol:一般设置为0即可,由系统自动选择协议。

domain参数

该参数指明要创建的sockfd的协议族,一般比较常用的有两个:

  • AF_INET:IPv4协议族
  • AF_INET6:IPv6协议族

type参数

该参数用于指明套接字类型,具体有:

  • SOCK_STREAM字节流套接字,适用于TCP或SCTP协议
  • SOCK_DGRAM数据报套接字,适用于UDP协议
  • SOCK_SEQPACKET:有序分组套接字,适用于SCTP协议
  • SOCK_RAW:原始套接字,适用于绕过传输层直接与网络层协议(IPv4/IPv6)通信

protocol参数

该参数用于指定协议类型。

如果是TCP协议的话就填写IPPROTO_TCP,UDP和SCTP协议类似。

也可以直接填写0,这样的话则会默认使用domain参数和type参数组合制定的默认协议

(参照上面type参数的适用协议)

返回值

socket函数在成功时会返回套接字描述符,失败则返回-1。失败的时候可以通过输出errno来详细查看具体错误类型。

例如下面的代码创建了一个TCP套接字,如果失败会返回负值:

SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 当 sockfd = -1 则代表socket创建失败
if (sockfd < 0) {perror("Socket creation failed");cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;return -1;
}

在底层C++使用两个宏来表示 socket 创建状态:

#define INVALID_SOCKET  (SOCKET)(~0)
#define SOCKET_ERROR            (-1)

2.绑定Socket:

服务端的Socket需要绑定到指定的IP地址和端口,以便客户端可以连接。绑定可以通过bind()函数来完成,把一个本地协议地址赋予一个套接字

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:Socket描述符,即前一步创建的Socket。
  • addr:结构体sockaddr指针,包含IP地址和端口信息。
  • addrlenaddr结构体的大小。

例如下面这段代码,将Socket绑定到本地IP地址和端口8080上:

首先看一下sockaddr_in的底层,我们一般根据这个结构体的底层的其中三个字段进行赋值然后与创建的套接字进行绑定:

struct sockaddr_in {uint8_t sin_len;  // 结构长度,非必需short sin_family; // 地址族,一般为AF_****格式,常用的是AF_INETUSHORT sin_port;  // 16位TCP或UDP端口号IN_ADDR sin_addr; // 32位IPv4地址CHAR sin_zero[8]; // 保留数据段,一般置零
};
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);        // 指定端口,大小端问题,将本地转换成路由器使用的大端(千百十个)
server_address.sin_addr.s_addr = INADDR_ANY;  // 绑定到本地所有可用的IP
// server_address.sin_addr.s_addr = inet_addr("0.0.0.0"); // 字符串IP地址转换成整数IP
// 返回-1代表绑定错误
if (bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {perror("Bind failed");cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;// 关闭连接closesocket(sockfd);return -1;
}

不同的计算机对数据的存储格式不一样,比如 32 位的整数 0x12345678,可以在内存里从高到低存储为 12-34-56-78 (大端)或者从低到高存储为 78-56-34-12(小端)。但是这对于网络中的数据来说就带来了一个严重的问题,当机器从网络中收到 12-34-56-78 的数据时,它怎么知道这个数据到底是什么意思?

解决的方案也比较简单,在传输数据之前和接受数据之后,必须调用 htonl/htons 或 ntohl/ntohs 先把数据转换成网络字节序或者把网络字节序转换为机器的字节序。


我们注意到上面代码不管是赋值IP还是端口,都不是直接赋值,而是使用了类似htons()htonl()的函数,这便是字节排序函数。

不同的机子上对于多字节变量的字节存储顺序是不同的,有大端字节序小端字节序两种。如果我们将机子A的变量原封不动传到机子B上,其值可能会发生变化,导致数据传输异常。故我们需要引入一个通用的规范,称为网络字节序。

#include <WinSock2.h>uint16_t htons(uint16_t host16bitvalue);    //host to network, 16bit
uint32_t htonl(uint32_t host32bitvalue);    //host to network, 32bit
uint16_t ntohs(uint16_t net16bitvalue);     //network to host, 16bit
uint32_t ntohl(uint32_t net32bitvalue);     //network to host, 32bit

3.服务端监听(listen)连接请求:

我们在绑定成功后,服务端Socket需要进入监听模式,以便等待客户端的连接请求。监听通过listen()函数实现。

int listen(int sockfd, int backlog);
  • sockfd:Socket描述符。
  • backlog:等待连接的最大队列长度。

例如下面的代码将Socket设置为监听模式,并允许最多3个客户端等待连接:

if (listen(sockfd, 3) < 0) {perror("Listen failed");cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;// 关闭连接closesocket(sockfd);return -1;
}

4.服务端接受(accept)客户端连接:

当客户端尝试连接时,服务端通过accept()函数接受连接。

SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:服务端的监听Socket。
  • addr:指向存储客户端信息的sockaddr结构体。
  • addrlenaddr结构体的大小。

例如下面代码,accept()是一个阻塞函数,会阻塞程序,直到有客户端连接。如果连接成功,会返回新的Socket用于与该客户端通信:

struct sockaddr_in client_address;
socklen_t client_address_len = sizeof(client_address);
while (1) {// 这里返回客户端的new_socket才是跟客户端可通讯的socketSOCKET new_socket = accept(sockfd, (struct sockaddr*)&client_address, &client_address_len);if (new_socket == INVALID_SOCKET) {perror("Accept failed");cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;// 直到创建可通讯的socket才能跳出循环continue;}// 开始通讯// 关闭连接
}

5.客户端连接(connect)服务端:

客户端通过connect()函数向服务端发起连接。在调用connect函数的时候,调用方(也就是客户端)便会主动发起TCP三次握手。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:客户端的Socket。
  • addr:指向服务端地址的sockaddr结构体。
  • addrlenaddr结构体的大小。

在操作上比较类似于服务端使用bind函数(虽然做的事情完全不一样),唯一的区别在于指定ip这块。服务端调用bind函数的时候无需指定ip,但客户端调用connect函数的时候则需要指定服务端的ip。在客户端的代码中,令套接字地址结构体指定ip的代码如下:

inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);

这个就涉及到ip地址的表达格式与数值格式相互转换的函数。

IP地址格式转换函数

IP地址一共有两种格式:

  • 表达格式:也就是我们能看得懂的格式,例如"192.168.19.12"这样的字符串
  • 数值格式:可以存入套接字地址结构体的格式,数据类型为整型

显然,当我们需要将一个IP赋进套接字地址结构体中,就需要将其转换为数值格式。#include <WinSock2.h>提供了两个函数用于IP地址格式的相互转换:

// 将IP地址从表达格式转换为数值格式
int inet_pton(int domain, const char *strptr, void *addrptr);
// 将IP地址从数值格式转换为表达格式
const char *inet_ntop(int domain, const void *addrptr, char *strptr, size_t len);

 下面代码的作用是将客户端连接到本地地址127.0.0.1的端口8080上:

struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr);if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {printf("Connect error(%d): %s\n", errno, strerror(errno));closesocket(sockfd);return -1;
}

6.数据传输:

一旦建立连接,客户端和服务端就可以使用send()recv()来交换数据。

  • send():用于发送数据。
  • recv():用于接收数据。
while (1) {struct sockaddr_in client_address;int client_address_len = sizeof(client_address);// 这里返回客户端的new_socket才是跟客户端可通讯的socketSOCKET new_socket = accept(sockfd, (struct sockaddr*)&client_address, client_address_len);if (new_socket == INVALID_SOCKET) {perror("Accept failed");cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;// 直到创建可通讯的socket才能跳出循环continue;}// 开始通信(B/S)char buffer[1024] = { 0 };recv(new_socket,buffer,1024,0);// 关闭连接closesocket(new_socket);
}

客户端发送请求,服务器接收请求并返回响应。由于 TCP 是面向流的协议,recvsend 需要处理流式数据。数据可能会被分割成多个包发送,也可能会接收到部分数据,因此需要处理数据的拼接和分包。recvsend 在默认情况下是阻塞的,调用者会等待数据的发送或接收完成。通过设置 MSG_DONTWAIT 等标志可以实现非阻塞模式。

(1)recv的使用:

下面是recv()的底层代码:

recv(SOCKET s,   // 客户端socketchar* buf,  // 接受的数据存到哪里int len,    // 接受的长度int flags   // 标记0);
  • 如果发送成功,返回实际发送的字节数。如果发送的数据量大于缓冲区大小,返回的值可能小于 len,此时需要调用 send 发送剩余的数据。
  • 如果发生错误,返回 -1,并且设置 errno,可以使用 perror()strerror() 获取错误信息。

 (2)send的使用:

send(SOCKET s,const char* buf,int len,int flags);
  • sockfd:表示一个已经建立连接的Socket描述符。你可以通过调用 socket() 创建一个Socket并通过 connect()(客户端)或 accept()(服务器)建立连接。
  • buf:一个指向数据的指针,表示要发送的数据缓冲区。通常是一个字符数组或字符串。
  • len:要发送的字节数。这个长度指的是缓冲区中的数据的大小。
  • flags:发送时的标志。常见的标志有:
    • 0:表示默认行为。
    • MSG_DONTWAIT:在非阻塞模式下使用,表示如果没有数据可以发送,send()函数将立刻返回,而不是阻塞。
    • MSG_NOSIGNAL:如果套接字已关闭,不会抛出信号(只适用于某些平台,如Linux)。

7.关闭(closesocket)Socket:

通信完成后,需要关闭Socket释放资源。可以使用closesocket()函数来关闭Socket。

closesocket(sockfd);

8.案例分析:

编写服务端代码,能够使打开浏览器访问http://127.0.0.1:8080/,可以显示“Hello, World!”。

TCP 服务器需要以下步骤:

  • 创建 Socket。
  • 绑定 Socket 到一个特定的地址和端口。
  • 监听端口,等待客户端连接。
  • 接受客户端连接并处理数据。
  • 关闭连接。
#include <iostream>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;int main() {WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {cerr << "WSAStartup failed: " << WSAGetLastError() << endl;return -1;}// 创建Socket套接字SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == INVALID_SOCKET) {cerr << "Socket creation failed: " << WSAGetLastError() << endl;WSACleanup();return -1;}// 绑定Socket到IP和端口struct sockaddr_in server_address = { 0 };server_address.sin_family = AF_INET;server_address.sin_addr.s_addr = inet_addr("0.0.0.0");server_address.sin_port = htons(8080);if (bind(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) == SOCKET_ERROR) {cerr << "Bind failed: " << WSAGetLastError() << endl;closesocket(sockfd);WSACleanup();return -1;}// 开始监听if (listen(sockfd, 3) < 0) {cerr << "Listen failed: " << WSAGetLastError() << endl;closesocket(sockfd);WSACleanup();return -1;}cout << "Waiting for connections on port 8080...\n";while (true) {struct sockaddr_in client_address;int client_address_len = sizeof(client_address);// 接受客户端连接SOCKET new_socket = accept(sockfd, (struct sockaddr*)&client_address, &client_address_len);if (new_socket == INVALID_SOCKET) {cerr << "Accept failed: " << WSAGetLastError() << endl;continue;}// 接收客户端请求char buffer[1024] = { 0 };recv(new_socket, buffer, sizeof(buffer), 0);cout << "Received request:\n" << buffer << endl;// 构造并发送简单的HTTP响应,包括状态行、响应头和响应体(内容为“Hello, World!”)const char* http_response ="HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Content-Length: 13\r\n""\r\n""Hello, World!";send(new_socket, http_response, strlen(http_response), 0);// 关闭连接closesocket(new_socket);}// 清理Socketclosesocket(sockfd);WSACleanup();return 0;
}

 编写客户端代码:

TCP 客户端需要以下步骤:

  • 创建 Socket。
  • 连接到服务器的 IP 地址和端口。
  • 发送数据给服务器。
  • 接收来自服务器的响应。
  • 关闭连接。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")using namespace std;int main() {WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {cerr << "WSAStartup failed: " << WSAGetLastError() << endl;return -1;}// 创建 SocketSOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket == INVALID_SOCKET) {cerr << "Socket creation failed: " << WSAGetLastError() << endl;WSACleanup();return -1;}// 设置服务器地址sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);  // 连接到服务器的 8080 端口serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 服务器地址:本地地址// 连接到服务器if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {cerr << "Connection failed: " << WSAGetLastError() << endl;closesocket(clientSocket);WSACleanup();return -1;}cout << "Connected to server." << endl;// 发送数据const char* message = "Hello, Server!";if (send(clientSocket, message, strlen(message), 0) == SOCKET_ERROR) {cerr << "Send failed: " << WSAGetLastError() << endl;closesocket(clientSocket);WSACleanup();return -1;}// 接收响应char buffer[1024] = {0};int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);if (bytesReceived > 0) {cout << "Server says: " << buffer << endl;} else if (bytesReceived == 0) {cout << "Connection closed by server." << endl;} else {cerr << "Receive failed: " << WSAGetLastError() << endl;}// 关闭连接closesocket(clientSocket);WSACleanup();return 0;
}

三.TCP协议流程:

TCP 协议是面向连接的,它在客户端和服务器之间建立一个可靠的连接。具体流程如下:

  • TCP三次握手(建立连接)

    • 客户端向服务器发送一个 SYN 请求包,表示要建立连接。
    • 服务器收到后,回复一个 SYN-ACK 包,表示同意连接。
    • 客户端收到后,发送一个 ACK 包,确认连接建立。

A(客户端): CLOSED -> SYN_SENT -> ESTABLISHED

B(服务端): LISTEN(监听状态) -> SYN_RCVD -> ESTABLISHED


首先,客户端主动调用connect发送一个SYN包(如TCP首部和TCP选项等协议包必须数据)来对服务端请求连接,此时客户端的状态从CLOSED转换成SYN_SENT。

在socket编程中,服务端调用过listen函数使其处于监听状态并且处于accept函数等待连接的阻塞状态下,才能收到SYN包返回一个针对该SYN包的响应包(ACK包)和一个新的SYN包,此时服务端状态由LISTEN转换成SYN_RCVD。

随后客户端收到服务端发来的两个包,并返回针对新的SYN包的ACK包。此时客户端的状态从SYN_SENT切换至ESTABLISHED,处于这个状态的客户端就可以传输数据了。

服务端收到ACK包后代表成功建立连接,这时调用accept函数返回客户端套接字,同样服务端的状态由SYN_RCVD切换至ESTABLISHED,同样处于这个状态的服务端就可以传输数据了。

  • 数据传输

    一旦连接建立,客户端和服务器就可以通过 send()recv() 来发送和接收数据
  • TCP四次挥手(断开连接)

    • 客户端或服务器中的任一方可以发送 FIN 包,表示断开连接。
    • 对方收到后,会回复一个 ACK 包确认断开。
    • 最后,发送 FIN 包的方再收到对方的 ACK 包,连接彻底关闭。

假设A主动关闭连接,A与B的流程转换图如下: 

A: ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED

B:ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED


在收发数据之后,如果需要断开连接,方中有一方(假设为A,另一方为B)主动关闭连接(调用close函数或者其进程本身被终止等情况)则其向B发送FIN包。此时A的状态从ESTABLISHED转换成FIN_WAIT_1。

B接收到A传递的FIN包后发送ACK包,此时B的状态从ESTABLISHED转换成CLOSE_WAIT。

随后A接收到B传递的ACK包后,此时A的状态从FIN_WAIT_1转换成FIN_WAIT_2。

过一段时间后,B调用close函数发送FIN包,此时B的状态从CLOSE_WAIT转换成LAST_ACK。

A接收到FIN包并发送ACK包,此时A的状态由FIN_WAIT_2转换成TIME_WAIT。

B接收到ACK包后关闭连接,此时B的状态从LAST_ACK转换成CLOSED。

A等待一段时间后关闭连接,此时A的状态从TIME_WAIT转换成CLOSED状态。

四.UDP协议流程:

UDP(用户数据报协议)是一种无连接的网络协议,与 TCP 相比,UDP 不会建立连接并且不保证数据的可靠性,适用于需要高速传输且容忍丢包的场景。UDP 编程相对简单,因为不需要像 TCP 一样进行连接和握手,但也因为没有连接管理,数据可能会丢失。

前置知识:

UDP 的特性: 

UDP 不需要在发送数据前建立连接,这使得它比 TCP 更加高效,尤其是在大量数据传输时。但UDP 不保证数据的可靠送达,因此不会进行重传、流量控制和拥塞控制。如果需要可靠性,应用层需要自己处理。UDP 发送的数据包可能会乱序接收,应用层需要处理顺序问题。 

UDP的使用:

UDP 是无连接的,它不需要像 TCP 那样进行握手,适合需要高效传输且容忍数据丢失的应用场景。UDP 不需要像 TCP 那样进行三次握手建立连接,也没有四次挥手断开连接。每次通信都是独立的,发送方和接收方都可以在任意时间发送和接收数据包。在 C++ 中使用 UDP 编程时,创建 Socket 使用 SOCK_DGRAM 类型,数据的发送和接收使用 sendto()recvfrom(),不需要使用 connect()。UDP没有可靠性保证,应用程序需要自行确保数据的完整性和顺序,或者使用更高层的协议来处理。 

接收和发送数据的函数:

在 Socket 编程中,recvfrom()sendto() 是 UDP 协议下用于接收和发送数据的函数。这些函数是 UDP 套接字通信的关键,它们支持无连接的数据传输,允许在网络上发送和接收数据报(Datagram)。这两者是 UDP 套接字编程中最常用的函数,因为它们简化了数据传输的过程,并支持多播和广播通信。

recvfrom() 函数

recvfrom() 是用于接收来自指定源(IP 地址和端口)的数据报的函数。在 UDP 通信中,数据报是独立的、无连接的,这意味着每个数据包可以有不同的来源和目标。

int recvfrom(SOCKET s,                        // 套接字描述符char *buf,                       // 缓冲区,用于存储接收到的数据int len,                          // 缓冲区的大小int flags,                        // 接收标志,通常为 0struct sockaddr *from,            // 来源地址(接收到数据报的来源地址)int *fromlen                      // 来源地址的长度
);

示例:UDP 服务器端使用 recvfrom():

recvfrom() 接收数据并存储在 buffer 中。clientAddr 保存了发送者的 IP 地址和端口号。bytesReceived 记录了成功接收的字节数。 

char buffer[1024];
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);// 接收数据
int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (sockaddr*)&clientAddr, &clientAddrSize);
if (bytesReceived == SOCKET_ERROR) {cerr << "Receive failed: " << WSAGetLastError() << endl;
} else {cout << "Received: " << buffer << endl;
}
sendto() 函数

sendto() 用于向指定的目标地址发送数据报。由于 UDP 是无连接的协议,数据包发送时需要显式地指定目标地址和端口。

int sendto(SOCKET s,                          // 套接字描述符const char *buf,                    // 需要发送的数据int len,                            // 数据的长度int flags,                          // 发送标志,通常设为 0const struct sockaddr *to,          // 目标地址(发送数据报的目标地址)int tolen                           // 目标地址的长度
);

 示例:UDP 客户端使用 sendto():

sendto() 将消息发送到 127.0.0.1 上的端口 8080。目标地址是 serverAddr,它包含了目标服务器的 IP 地址和端口号。

const char* message = "Hello, Server!";
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");// 发送数据
int bytesSent = sendto(clientSocket, message, strlen(message), 0, (sockaddr*)&serverAddr, sizeof(serverAddr));
if (bytesSent == SOCKET_ERROR) {cerr << "Send failed: " << WSAGetLastError() << endl;
} else {cout << "Message sent to server." << endl;
}

 

1.UDP的服务端:

UDP 服务器端的主要工作是:

  • 创建一个 Socket。
  • 绑定(bind) Socket 到一个指定的端口。
  • 接收来自客户端的数据(没有连接过程)。
  • 发送数据回客户端(如果需要)。
  • 初始化 Winsock:调用 WSAStartup() 初始化 Winsock 库。
  • 创建 UDP Socket:使用 socket() 创建一个 UDP Socket(SOCK_DGRAM 表示数据报类型的套接字)。
  • 设置服务器地址:设置服务器的 IP 地址和端口,使用 INADDR_ANY 来绑定所有网络接口。
  • 绑定 Socket:通过 bind() 将 Socket 绑定到指定的地址和端口。
  • 接收数据:通过 recvfrom() 接收来自客户端的数据,不需要建立连接,因为 UDP 是无连接的。接收的数据会被存放在 buffer 中,clientAddr 用来保存客户端的地址信息。
  • 发送响应:使用 sendto() 向客户端发送数据。由于是无连接的,每次发送时都需要指定客户端的地址信息。
  • 关闭 Socket:程序结束后通过 closesocket() 关闭 Socket。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")using namespace std;int main() {// 调用 WSAStartup() 初始化 Winsock 库。WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {cerr << "WSAStartup failed: " << WSAGetLastError() << endl;return -1;}// 使用 socket() 创建一个 UDP Socket(SOCK_DGRAM 表示数据报类型的套接字)。SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if (serverSocket == INVALID_SOCKET) {cerr << "Socket creation failed: " << WSAGetLastError() << endl;WSACleanup();return -1;}// 设置服务器的 IP 地址和端口,使用 INADDR_ANY 来绑定所有网络接口。sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);  // 设置端口serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定所有网络接口// 绑定 Socket,通过 bind() 将 Socket 绑定到指定的地址和端口if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {cerr << "Bind failed: " << WSAGetLastError() << endl;closesocket(serverSocket);WSACleanup();return -1;}cout << "Server listening on port 8080..." << endl;// 接收数据char buffer[1024] = {0};sockaddr_in clientAddr;int clientAddrSize = sizeof(clientAddr);while (true) {// 通过 recvfrom() 接收来自客户端的数据,不需要建立连接,因为 UDP 是无连接的。// 接收的数据会被存放在 buffer 中,clientAddr 用来保存客户端的地址信息。int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (sockaddr*)&clientAddr, &clientAddrSize);if (bytesReceived == SOCKET_ERROR) {cerr << "Receive failed: " << WSAGetLastError() << endl;continue;}cout << "Received: " << buffer << endl;// 向客户端发送响应const char* response = "Message received!";// 使用 sendto() 向客户端发送数据。由于是无连接的,每次发送时都需要指定客户端的地址信息。int bytesSent = sendto(serverSocket, response, strlen(response), 0, (sockaddr*)&clientAddr, sizeof(clientAddr));if (bytesSent == SOCKET_ERROR) {cerr << "Send failed: " << WSAGetLastError() << endl;}}// 程序结束后通过 closesocket() 关闭 Socket。closesocket(serverSocket);// 释放Winsock资源WSACleanup();return 0;
}

2.UDP的客户端:

UDP 客户端的主要工作是:

  • 创建一个 Socket。
  • 向服务器发送数据。
  • 接收来自服务器的响应。
  • 初始化 Winsock:同样需要初始化 Winsock。
  • 创建 UDP Socket:创建一个 UDP 类型的 Socket。
  • 设置服务器地址:设置服务器的 IP 地址和端口。
  • 发送数据:使用 sendto() 向服务器发送数据。UDP 不需要建立连接,因此发送时直接指定服务器的地址。
  • 接收数据:通过 recvfrom() 接收来自服务器的响应。
  • 关闭 Socket:最后关闭客户端的 Socket。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")using namespace std;int main() {WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {cerr << "WSAStartup failed: " << WSAGetLastError() << endl;return -1;}// 创建 UDP SocketSOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if (clientSocket == INVALID_SOCKET) {cerr << "Socket creation failed: " << WSAGetLastError() << endl;WSACleanup();return -1;}// 设置服务器地址sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);  // 服务器端口号serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 服务器地址// 发送数据const char* message = "Hello, Server!";int bytesSent = sendto(clientSocket, message, strlen(message), 0, (sockaddr*)&serverAddr, sizeof(serverAddr));if (bytesSent == SOCKET_ERROR) {cerr << "Send failed: " << WSAGetLastError() << endl;closesocket(clientSocket);WSACleanup();return -1;}// 接收响应char buffer[1024] = {0};sockaddr_in serverResponseAddr;int serverResponseAddrSize = sizeof(serverResponseAddr);int bytesReceived = recvfrom(clientSocket, buffer, sizeof(buffer), 0, (sockaddr*)&serverResponseAddr, &serverResponseAddrSize);if (bytesReceived == SOCKET_ERROR) {cerr << "Receive failed: " << WSAGetLastError() << endl;} else {cout << "Server says: " << buffer << endl;}// 关闭 Socketclosesocket(clientSocket);WSACleanup();return 0;
}

 五.前置基础知识总结:

详细知识可参考下面文档:C/C++网络编程基础知识超详细讲解第一部分(系统性学习day11)-阿里云开发者社区 

1. 网络基础知识:

  • IP 地址:计算机在网络中的唯一标识。
  • 端口号:应用程序通过端口号与网络中的其他计算机进行通信。
  • 协议:通信协议用于规范计算机间如何传输数据,最常用的协议有 TCP 和 UDP。
  • 路由和网络层次结构:数据如何在不同的网络设备之间转发,网络中的路由和地址解析协议(如 ARP)是如何工作的。
  • 网络模型:OSI 七层模型和 TCP/IP 四层模型。
    • OSI 模型:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
    • TCP/IP 模型:网络接口层、互联网层、传输层、应用层。

2. Socket 的基本概念:

Socket 是网络通信的接口,是操作系统提供的用于建立网络通信的抽象层。需要了解以下概念:

  • 套接字(Socket):是应用程序与网络进行通信的端点。
  • Socket 类型:常见的有:
    • 流式套接字(SOCK_STREAM):基于 TCP 协议,保证数据传输可靠,适用于需要高可靠性的应用。
    • 数据报套接字(SOCK_DGRAM):基于 UDP 协议,不保证数据的可靠传输,适用于快速、简单的通信。
  • 协议族(Protocol Family):通常使用 AF_INET(IPv4)或 AF_INET6(IPv6)等。
  • 连接类型
    • 面向连接的(Connection-oriented):如 TCP,传输之前需要建立连接。
    • 无连接的(Connectionless):如 UDP,数据包在发送时不需要先建立连接。

3. Socket 编程中的常用 API:

在学习 Socket 编程时,我们会用到很多函数来创建、绑定、监听、发送、接收、关闭套接字等。以下是常用的 Socket 编程函数:

创建和管理 Socket:

  • socket():创建一个套接字。
  • bind():将套接字与本地地址(IP 地址和端口)绑定。
  • listen():在服务器端用于监听端口,准备接受连接。
  • accept():在服务器端接受客户端的连接请求。
  • connect():在客户端连接到服务器端。
  • close():关闭套接字。

发送和接收数据:

  • send():在已连接的 TCP 套接字上发送数据。
  • recv():从已连接的 TCP 套接字接收数据。
  • sendto():在 UDP 套接字上发送数据,需要指定目标地址。
  • recvfrom():从 UDP 套接字接收数据,返回数据源的地址信息。

地址结构和网络信息:

  • sockaddr_in:用于指定 IPv4 地址和端口。
  • gethostbyname():根据域名获取 IP 地址。
  • inet_pton()inet_ntop():用于在点分十进制(Dotted Decimal)和二进制表示之间转换 IP 地址。

4. 常见的 Socket 编程模式:

客户端-服务器模式:

服务器端等待客户端的连接,客户端向服务器发起连接请求,建立起通信后交换数据。

广播和多播:

UDP 支持广播和多播通信:

  • 广播:发送数据包到网络中的所有主机(指定广播地址)。
  • 多播:发送数据包到特定的组播地址,只有加入该组的主机才能接收到数据。

5. 网络调试和错误处理 :

在编写 Socket 程序时,调试和处理错误是非常重要的:

  • 错误处理:每个 Socket 函数调用后,都应该检查其返回值,如果返回错误,需要根据 errnoWSAGetLastError() 获取错误信息。
  • 网络工具:如 pingtraceroutenetstat 等工具帮助检查网络连接状况。

相关文章:

C++ --- Socket套接字的使用

目录 一.什么是Socket套接字&#xff1f; 二.Socket的使用&#xff1a; 前置步骤&#xff1a; 为什么要加入 WSAStartup 和 WSACleanup &#xff1f; 1.创建Socket&#xff1a; 2.绑定Socket&#xff1a; 3.服务端监听连接请求&#xff1a; 4.服务端接受客户端连接&…...

MG协议转换器:制氢行业的数字桥梁

在新能源产业蓬勃发展的今天&#xff0c;制氢行业正迎来前所未有的发展机遇。作为清洁能源的重要组成部分&#xff0c;氢气的生产与利用不仅关乎环境保护&#xff0c;更是推动能源结构转型的关键一环。然而&#xff0c;在制氢行业的数字化转型进程中&#xff0c;数据的传输与处…...

人工智能技术的未来:变革生活与工作的潜力

随着人工智能&#xff08;AI&#xff09;技术的不断发展&#xff0c;我们已经见证了其在各行各业的巨大变革。无论是在医疗、商业还是日常生活中&#xff0c;AI都正在悄然改变着我们的工作方式和生活方式。未来&#xff0c;人工智能的应用前景广阔&#xff0c;它将继续深入我们…...

D60【python 接口自动化学习】- python基础之数据库

day60 数据库定义 学习日期&#xff1a;20241106 学习目标&#xff1a;MySQL数据库-- 128&#xff1a;数据库定义 学习笔记&#xff1a; 无处不在的数据库 数据库如何存储数据 数据库管理系统&#xff08;数据库软件&#xff09; 数据库和SQL的关系 总结 数据库就是指数据…...

零基础大龄程序员如何转型AI大模型,系统学习路径与资源推荐!!

前言 随着科技的飞速发展&#xff0c;AI大模型浪潮席卷全球&#xff0c;相关岗位炙手可热。在这个背景下&#xff0c;许多大龄程序员开始思考如何转型&#xff0c;以适应时代的变化。结合自身编程基础&#xff0c;大龄程序员可以学习机器学习、深度学习算法&#xff0c;投身于…...

vue3+vant实现使用van-picker实现三级级联菜单展示(含递归遍历)

1、递归遍历三级展示&#xff0c;禁用自动弹起软键盘、设置文档自动换行避免过长文本省略号展示 <div class"text_div"><van-fieldclass"span_text":center"true"v-model"jobLevelCodeText"is-linklabel"任职岗位"…...

oracle-函数-grouping sets(x1,x2,x3...)的妙用

GROUPING SETS 允许你为多个列组合生成分组汇总。它类似于多个 GROUP BY 子句的 UNION ALL 操作&#xff0c;但更加简洁和高效 首先&#xff1a;创建表及接入测试数据 create table students (id number(15,0), area varchar2(10), stu_type varchar2(2), score number(20,2))…...

人工智能在医疗病例诊断中的应用与展望

人工智能在医疗病例诊断中的应用与展望 摘要&#xff1a; 本文探讨了人工智能在医疗病例诊断中的卓越应用、显著优势、面临的挑战及应对策略&#xff0c;以及未来展望。人工智能在医学影像诊断、病理诊断和辅助临床诊断方面展现出巨大潜力&#xff0c;为医学研究和临床治疗带来…...

OceanBase 安装使用详细说明

OceanBase 安装使用详细说明 一、系统环境要求二、安装OceanBase环境方案一:在线下载并安装all-in-one安装包方案二:离线安装all-in-one安装包安装前的准备工作三、配置OceanBase集群编辑配置文件部署和启动集群连接到集群集群状态和管理四、创建业务租户和数据库创建用户并赋…...

CI_CD

什么是CI/CD 在前端开发中&#xff0c;CI/CD 是 Continuous Integration&#xff08;持续集成&#xff09;和 Continuous Deployment/Continuous Delivery&#xff08;持续部署/持续交付&#xff09;的简称。它是一种软件开发实践&#xff0c;自动化了应用的构建、测试和发布过…...

Linux -- 初识线程

目录 线程的初步认识 为什么需要线程 怎么让代码分成多个执行流并发执行呢&#xff1f; 管理线程 线程的初步认识 线程是进程内部的一个执行分支&#xff0c;线程是CPU调度的基本单位。 在Linux操作系统中&#xff0c;线程是程序执行流的最小单位。一个进程可以包含多个线…...

Uniapp底部导航栏设置(附带PS填充图标教程)

首先需要注册和登录ifconfont官网&#xff0c;然后创建项目添加需要的图标 创建和添加图标库请参考&#xff1a;Uniapp在Vue环境中引入iconfont图标库&#xff08;详细教程&#xff09; 打开iconfont官网&#xff0c;找到之前添加的图标库&#xff0c;下载png图片 如果需要的…...

单智能体carla强化学习实战工程介绍

有三个工程&#xff1a; Ray_Carla: 因为有的论文用多进程训练强化学习&#xff0c;包括ray分布式框架等&#xff0c;这里直接放了一个ray框架的示例代码&#xff0c;是用sac搭建的&#xff0c;obs没用图像&#xff0c;是数值状态向量值&#xff08;速度那些&#xff09;。 …...

潮玩宇宙方块兽系统开发:可定制UI与多种游戏内嵌助力个性化体验

潮玩宇宙方块兽系统开发正在推动潮玩与游戏的融合&#xff0c;通过个性化的UI设计和多游戏内嵌模式&#xff0c;为用户带来了独一无二的体验。本文将从可定制UI、多游戏内嵌功能以及系统实现等方面入手&#xff0c;探讨如何构建一个极具吸引力的潮玩宇宙方块兽系统。 一、可定制…...

什么是低代码?3000字低代码超全解读!

现在这个时代企业面对的挑战越来越复杂&#xff0c;尤其在软件开发和应用交付方面&#xff0c;因为传统开发过程复杂且费时&#xff0c;企业很难从传统的软件开发方式中迅速响应市场变化从而获利。 而低代码&#xff08;Low-Code&#xff09;平台的出现为企业提供了一种更加快…...

雷池社区版7.1新版本自定义NGINX配置分析

简单介绍雷池&#xff0c;是一款简单好用, 效果突出的 Web 应用防火墙(WAF)&#xff0c;可以保护 Web 服务不受黑客攻击。 雷池通过阻断流向 Web 服务的恶意 HTTP 流量来保护 Web 服务。雷池作为反向代理接入网络&#xff0c;通过在 Web 服务前部署雷池&#xff0c;可在 Web 服…...

[SAP ABAP] 面向对象程序设计-类和对象

面向对象开发的特点&#xff1a;封装、继承和多态 什么是类和对象&#xff1f; 类(CLASS)是创建对象的模板&#xff0c;对象(OBJECT)是类的实例 一个类可以创建多个对象 类 > 类型 对象 > 个体 在ABAP语言中&#xff0c;定义一个类&#xff0c;需要包含定义(defin…...

『大模型笔记』IBM技术团队:什么是智能体型RAG!

『大模型笔记』IBM技术团队:什么是智能体型RAG! 文章目录 一. 『大模型笔记』IBM技术团队:什么是智能体型RAG!二. 参考文献一. 『大模型笔记』IBM技术团队:什么是智能体型RAG! ✅检索增强生成(RAG)是一种结合检索和生成能力的技术,通过从向量数据库检索相关信息作为上…...

WPF 中 NavigationWindow 与 Page 的继承关系解析

官网解析&#xff1a; NavigationWindow 类 | Page 类 public class BaseWindow: NavigationWindow{} public partial class CountPage : Page{} 都是创建的WPF界面有什么区别&#xff1f; 在 WPF&#xff08;Windows Presentation Foundation&#xff09;开发中&#…...

WebRTC基础理论和通话原理

WebRTC理论知识 1.什么是WebRTC? WebRTC&#xff08;Web RealTime Communication&#xff09;是 Google于2010以6829万美元从 Global IP Solutions 公司购买&#xff0c;并 于2011年将其开源&#xff0c;旨在建立一个互联网浏览器间的实时通信的平台&#xff0c;让 WebRTC技术…...

NPU 可不可以代替 GPU

结论 先说结论&#xff0c;GPU分为可以做图形处理的传统意义上的真GPU&#xff0c;做HPC计算的GPGPU和做AI加速计算的GPGPU&#xff0c;所以下面分别说&#xff1a; 对于做图形处理的GPU&#xff0c;这个就和NPU 一样&#xff0c;属于DSA&#xff0c;没有替代性。当然&#xf…...

Vue3版本的uniapp项目运行至鸿蒙系统

新建Vue3版本的uniapp项目 注意&#xff0c;先将HbuilderX升级至最新版本&#xff0c;这样才支持鸿蒙系统的调试与运行&#xff1b; 按照如下图片点击&#xff0c;快速升级皆可。 通过HbuilderX创建 官方文档指导链接 点击HbuilderX中左上角文件->新建->项目 创建vue3…...

部署stable-diffusion3.5 大模型,文生图

UI 使用推荐的ComfyUI&#xff0c;GitHub 地址&#xff0c;huggingface 需要注册登录&#xff0c;需要下载的文件下面有说明 Dockerfile 文件如下&#xff1a; FROM nvidia/cuda:12.4.0-base-ubuntu22.04 RUN apt-get update && apt-get install python3 pip git --n…...

数据采集之selenium模拟登录

使用Cookijar完成模拟登录 本博文爬取实例为内部网站&#xff0c;请sduter使用本人账号替换*********&#xff08;学号&#xff09;&#xff0c;***&#xff08;姓名&#xff09;进行登录 from selenium import webdriver from selenium.webdriver.common.by import By from…...

机器学习中的两种主要思路:数据驱动与模型驱动

在机器学习的研究和应用中&#xff0c;如何从数据中提取有价值的信息并做出准确预测&#xff0c;是推动该领域发展的核心问题之一。在这个过程中&#xff0c;机器学习方法主要依赖于两种主要的思路&#xff1a;数据驱动与模型驱动。这两种思路在不同的应用场景中发挥着至关重要…...

【计算机网络】TCP协议面试常考(一)

三次握手和四次挥手是TCP协议中非常重要的机制&#xff0c;它们在多种情况下确保了网络通信的可靠性和安全性。以下是这些机制发挥作用的一些关键场景&#xff1a; 三次握手的必要性&#xff1a; 同步序列号&#xff1a; 三次握手确保了双方的初始序列号&#xff08;ISN&#…...

C#/.NET/.NET Core学习路线集合,学习不迷路!

前言 C#、.NET、.NET Core、WPF、WinForm、Unity等相关技术的学习、工作路线集合&#xff08;持续更新&#xff09;&#xff01;&#xff01;&#xff01; 全面的C#/.NET/.NET Core学习、工作、面试指南&#xff1a;https://github.com/YSGStudyHards/DotNetGuide C#/.NET/.N…...

使用哈希表做计数排序js

function hashSort(arr) {// 创建一个哈希表&#xff08;对象&#xff09;&#xff0c;统计每个数字出现的次数let hashMap {};arr.forEach(num > {if (hashMap[num]) {hashMap[num] 1;} else {hashMap[num] 1;}});// 根据哈希表的键值对构建排序后的数组let sortedArr …...

京津冀自动驾驶技术行业盛会|2025北京自动驾驶技术展会

“自动驾驶技术”已经成为全球汽车产业的焦点之一。在这个充满创新与变革的时代&#xff0c;“2025北京国际自动驾驶技术展览会”拟定于6月份在北京亦创国际会展中心盛大开幕&#xff0c;为全球自动驾驶技术领域的专业人士、企业以及爱好者们提供了一个交流与展示的平台。作为一…...

Chrome与火狐哪个浏览器的隐私追踪功能更好

当今数字化时代&#xff0c;互联网用户越来越关注在线隐私保护。浏览器作为我们探索网络世界的重要工具&#xff0c;其隐私追踪功能的强弱直接影响到个人信息的安全。本文将对比Chrome和Firefox这两款流行的浏览器&#xff0c;在隐私追踪防护方面的表现&#xff0c;并探讨相关优…...