Linux网络 UDP socket

背景知识

我们知道, IP 地址用来标识互联网中唯一的一台主机, port 用来标识该主机上唯一的一个网络进程,IP+Port 就能表示互联网中唯一的一个进程。所以通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp,srcPort, dstIp , dstPort} 这样的 4 元组就能标识互联网中唯二的两个进程。所以,网络通信的本质,也是进程间通信,我们把 ip+port 叫做套接字 socket。

socket
n.(电源 ) 插座; ( 电器上的 ) 插口,插孔,管座;槽;窝;托座;臼;孔穴
vt.把... 装入插座;给 ... 配插座
套接字(socket)是一种通信机制,它提供了一种在网络上进行进程间通信的方法。套接字可以被看作是网络通信的端点,它允许不同主机上的进程通过网络进行通信。套接字屏蔽了各个协议的通信细节,提供了TCP/IP协议的抽象,对外提供了一套接口,通过这个接口就可以统一、方便地使用TCP/IP协议的功能。
传输层的典型代表
如果我们了解系统,也了解网络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用来进行的网络通信。

TCP 协议
我们先对 TCP(Transmission Control Protocol 传输控制协议 ) 有一个直观的认识

  • 传输层协议

  • 有连接

  • 可靠传输

  • 面向字节流
    UDP 协议
    我们也对 UDP(User Datagram Protocol 用户数据报协议 ) 有一个直观的认识

  • 传输层协议

  • 无连接

  • 不可靠传输

  • 面向数据报

sockaddr 结构

套接字有很多类型,主要分为以下几种

  • Unix Socket域套接字:用于本地通信,通常用于同一台机器上的不同进程间通信。
  • Inet Socket网络套接字:用于网络通信,支持多种协议,如TCP和UDP。
  • Raw Socket原始套接字:用于网络管理和底层网络编程。

sockaddr 结构是在网络编程中用于表示套接字地址的通用数据结构, 它的作用是存储网络地址信息,供套接字函数使用,此时套接字函数就知道要对哪一台主机进行网络操作,它被设计为一个抽象层,允许应用程序通过同一接口处理不同类型的网络协议和地址族。

但是sockaddr结构体不能直接存储 IPv4 或 IPv6 的地址信息,在实际使用中,通常会用到它的具体子类型,如 sockaddr_in(用于 IPv4)和 sockaddr_in6(用于 IPv6),sockaddr_un(用于域套接)。

为了管理多种套接字,所有套接字的头部都是一个16位的地址类型,用于辨别这个结构体表示哪一个套接字。当操作sockaddr的时候,读取前16位就知道这个sockaddr具体是哪一种套接字,随后再进行类型转化,变成对应套接字类型的结构体,此时就能对具体的套接字做操作了。
socket API 可以都用 struct sockaddr * 类型表示 , 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性 , 可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数 ;

sockaddr 结构体定义在 <sys/socket.h> 头文件中,其基本定义如下:

cpp 复制代码
struct sockaddr {
    sa_family_t sin_family; /* 地址家族,AF_XXX */
    char sin_zero[14]; /* 填充字段,实际用途取决于具体的地址家族 */
};

其中,sin_family 字段用来指定协议族,即协议类型,常见的取值有 AF_INET(IPv4)、AF_INET6(IPv6)和 AF_UNIX(UNIX 域套接字)等。

其中最常用的就是 AF_INET 进行IPv4通信。其对应的具体结构体为struct sockaddr_in,定义如下:

cpp 复制代码
struct sockaddr_in {
    sa_family_t sin_family; /* 协议族,AF_INET */
    uint16_t sin_port; /* TCP 或 UDP 端口号 */
    struct in_addr sin_addr; /* 32 位 IPv4 地址 */
};

此处有一个小细节,IPv4的地址占32位,用一个int类型即可存储,sin_addr的类型却是struct in_addr,这其实是Linux对其进行了额外的一层封装:

cpp 复制代码
struct in_addr {
	uint32_t s_addr;
};

所以存储地址的时候,要用sockaddr_in.sin_addr.s_addr,此处嵌套了两层结构体。基于IP地址和端口号,此时就可以定位到全世界的一个主机上的一个具体进程,此时就可以进行后续的网络通信了。

bzero

我们知道struct sockaddr_in 的内部还有8字节填充,这是为了以确保struct sockaddr_in的大小与struct sockaddr一致,所以我们需要一开始时将其初始化为0。除此,创建结构体时分配到的内存原先有可能存储了其他数据,为了保证不被之前的数据影响,我们也要把整个结构体的内存全部置为0

所以我们可以使用bzero函数。

cpp 复制代码
void bzero(void* s, size_t n);
  • s:要初始化内存的地址
  • n:要初始化的字节数

示例如下

cpp 复制代码
struct sockaddr_in socket;
bzero(&socket,sizeof(socket));

网络字节序(填sin_port)

我们知道 , 内存中的多字节数据相对于内存地址有大端和小端之分 , 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。 那么如何定义网络数据流的地址呢 ?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此, 网络数据流的地址应这样规定 : 先发出的数据是低地址 , 后发出的数据是高地址. TCP/IP 协议规定 , 网络数据流应采用大端字节序 , 即低地址高字节 .

不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/ 接收数据,如果当前发送主机是小端, 就需要先将数据转成大端 ; 否则就忽略 , 直接发送即可。
为使网络程序具有可移植性 , 使同样的 C 代码在大端和小端计算机上编译后都能正常运行, 可以调用以下库函数做网络字节序和主机字节序的转换,使用以下函数需要包含头文件<arpa/inet.h>。

cpp 复制代码
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

这些函数名很好记 ,h 表示 host,n 表示 network,l 表示 32 位长整数 ,s 表示 16 位短整数。例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序 , 例如将 IP 地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换 , 将参数原封不动地返回。
假设我们有一个类型为 struct sockaddr_in 的套接字socket,在填写内部端口号时,内部数据的字节序就要使用网络字节序。因为 sin_port 类型为 uint16_t,所以使用 htons 函数。

cpp 复制代码
struct sockaddr_in socket;
socket.sin_port=8080;//错误
socket.sin_port=htons(8080);//正确

IP地址转换**(填sin_addr)**

在给 struct sockaddr_in 结构体填入数据时,其IP地址 sin_addr 的格式也需要遵循特定的规则。我们知道,IP地址有两种基本格式,4字节序列,以及点分十进制,如果拿到的IP地址格式与自己所需的类型不符,此时就要考虑两种格式之间转化的问题了。

inet_addr函数用于将一个点分十进制的IP地址字符串转换为网络字节序的32位整数。

cpp 复制代码
in_addr_t inet_addr(const char *cp);

参数cp是一个指向以点分十进制格式表示的IP地址字符串的指针,例如"127.0.0.1"。函数返回一个32位的无符号整数,表示转换后的IP地址。如果输入的字符串不是一个合法的IP地址,函数将返回INADDR_NONE,通常定义为-1。

示例如下

cpp 复制代码
struct sockaddr_in socket;
socket.sin_addr.s_addr = inet_addr("127.0.0.1");

我们知道存入 struct sockaddr_in 中的数据必须是网络字节序,此处将点分十进制转化为四字节序列后,应该还需要转成网络字节序。的确如此,不过我们不需要手动转换,因为 inet_addr 函数已经帮我们完成转换。

inet_ntoa函数用于将一个网络字节序的32位整数IP地址转换为点分十进制的字符串格式。这个函数的原型如下

cpp 复制代码
char *inet_ntoa(struct in_addr in);

参数in是一个struct in_addr类型的结构体,其中包含了一个32位的无符号整数,表示IP地址。inet_ntoa函数返回一个指向静态存储区的字符串,该字符串包含了以点分十进制格式表示的IP地址。由于返回的字符串存储在静态存储区,因此在多线程环境中可能会出现问题,因为后续的调用可能会覆盖之前的结果,所以在多线程环境下推荐使用ntop函数。

cpp 复制代码
char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:

  • af:协议族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
  • src:指向要转换的网络字节序IP地址的指针。
  • dst:指向存储转换后字符串的缓冲区的指针。
  • size:缓冲区的大小。

返回值:成功时,返回指向dst的非空指针。失败时,返回NULL,并且可以通过errno获取错误码。

综上,我们就有一个类型为 struct sockaddr_in 比较完整的初始化过程了:

cpp 复制代码
struct sockaddr_in socket;
bzero(&socket,sizeof(socket));
socket.sin_family=AF_INET;
socket.sin_port=htons(8080);
socket.sin_addr.s_addr=inet_addr("127.0.0.1");

UDP socket

UDP(User Datagram Protocol)套接字是一种网络通信协议,它提供了一种无连接、不可靠的传输服务。UDP套接字通常用于需要快速传输和实时响应的应用场景,如在线游戏、视频会议和实时监控等。

UDP套接字的特点

  1. 无连接性:UDP不需要在发送数据之前建立连接,因此减少了通信延迟。
  2. 不可靠性:UDP不提供数据传输的可靠性保证,数据包可能会丢失或乱序到达。
  3. 面向数据报:UDP以数据报为单位进行传输,每个数据报都是独立的。
  4. 全双工:UDP支持双向通信,允许同时进行数据的发送和接收。

socket 创建套接字

socket函数用于创建一个新的套接字,需要头文件<sys/types.h><sys/socket.h>,函数原型如下:

cpp 复制代码
int socket(int domain, int type, int protocol);

参数:

  • domain:指定协议族,对于UDP套接字,通常使用AF_INET(IPv4)或者AF_INET6(IPv6)。
  • type:指定套接字类型,创建UDP套接字时使用SOCK_DGRAM,DGRAMdatagram缩写,即数据报。
  • protocol:指定协议类型,一般设置为0,表示根据前面两个参数自动选择合适的协议(对于AF_INETSOCK_DGRAM,会自动选择UDP协议)。

返回值:如果成功创建套接字,返回一个非负的套接字描述符,其本质也是一个文件描述符,后续对网络的操作就是对这个文件的操作。比如向网络中发送消息,其实就是向文件中写入数据;如果失败,返回 - 1,并设置errno来指示错误类型。

使用示例如下

cpp 复制代码
int sockfd=socket(AF_INET,SOCK_DGRAM,0);

bind 绑定地址

当创建完套接字后,这个套接字还没有指定和哪一个主机通信,此时就需要IP地址和端口号,之前讲的sockaddr_in就派上用场了!bind函数用于给套接字绑定IP地址和端口号,指定和哪一台主机通信,函数原型如下:

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:由socket()函数返回的套接字描述符。
  • addr:一个指向sockaddr结构(或者sockaddr_in对于IPv4或者sockaddr_in6对于IPv6)的指针,包含了要绑定的地址和端口信息。
  • addrlen:是addr所指向结构的长度。

返回值:如果绑定成功,返回0;如果失败,返回 - 1,并设置errno来指示错误类型。

此处注意传入的是struct sockaddr *,所以sockaddr_in类型的变量传入的时候要进行类型转化。

cpp 复制代码
//创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

//初始化套接字要通信的目标主机地址
struct sockaddr_in socket;
bzero(&socket, sizeof(socket));
socket.sin_family = AF_INET;
socket.sin_port = htons(8080);
socket.sin_addr.s_addr = inet_addr("127.0.0.1");

//绑定地址到套接字
bind(sockfd, (struct sockaddr*)&socket, sizeof(socket));

sendto 发送数据

sendto函数用于发送数据报,函数原型如下:

cpp 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

  • sockfd:是要发送数据的套接字描述符。
  • buf:是一个指向要发送数据的缓冲区的指针。
  • len:是要发送数据的长度(以字节为单位)。
  • flags:一般设置为0,或者可以使用一些特定的标志(如MSG_DONTWAIT等)。
  • dest_addr:是一个指向sockaddr结构(或者sockaddr_in对于IPv4或者sockaddr_in6对于IPv6)的指针,包含了目标地址和端口信息。
  • addrlen:是dest_addr所指向结构的长度。

返回值:如果成功发送数据,返回实际发送的字节数;如果失败,返回 - 1,并设置errno来指示错误类型。

cpp 复制代码
//创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

//初始化套接字要通信的目标主机地址
struct sockaddr_in socket;
bzero(&socket, sizeof(socket));
socket.sin_family = AF_INET;
socket.sin_port = htons(8080);
socket.sin_addr.s_addr = inet_addr("127.0.0.1");

//给目标主机发送消息
const char* message = "hello";
sendto(sockfd,message,sizeof(message),(struct sockaddr*)&socket,sizeof(socket));

此处给地址为127.0.0.1端口为8080发送了一个报文,内容是"hello"。

我们可以看到以上代码中没有bind绑定地址,因为该操作已经由操作系统自动完成了,Linux会自动为其分配端口号,并完成绑定,随后通过随机分配的端口发送数据,这种行为称为隐式绑定。在实际开发中,一般服务端占用指定的端口,这样客户端才知道往哪一个端口发送请求,所以服务端要显式bind指定端口,不能让操作系统分配。而客户端往往不在意端口号,只需要能与服务端通信即可,所以客户端一般不bind,而是让系统随机分配一个端口。

recvfrom 接收数据

在Linux系统下,recvfrom函数用于在UDP套接字上接收数据,其函数原型如下:

cpp 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数:

  • sockfd:是要接收数据的套接字描述符。
  • buf:是一个指向用于接收数据的缓冲区的指针。
  • len:是缓冲区的长度(以字节为单位)。
  • flags:一般设置为0,或者可以使用一些特定的标志(如MSG_DONTWAIT等)。
  • src_addr:是一个指向sockaddr结构(或者sockaddr_in对于IPv4或者sockaddr_in6对于IPv6)的指针,如果不为NULL,则用于存储发送方的地址和端口信息。
  • addrlen:是一个指向socklen_t类型的指针,如果src_addr不为NULL,则在函数调用前,*addrlen应设置为src_addr所指向结构的长度;函数返回时,*addrlen被更新为实际存储发送方地址信息的结构的长度。

返回值:如果成功接收数据,返回实际接收的字节数;如果失败,返回 - 1,并设置errno来指示错误类型。

使用示例

cpp 复制代码
//创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

//初始化套接字要通信的目标主机地址
struct sockaddr_in socket;
bzero(&socket, sizeof(socket));
socket.sin_family = AF_INET;
socket.sin_port = htons(8080);
socket.sin_addr.s_addr = inet_addr("127.0.0.1");

//接收消息
struct sockaddr_in sendsock;
socklen_t len;
char* buf[1024];
recvfrom(sockfd,buf,sizeof(buf)-1,(struct sockaddr*)&sendsock,sizeof(len));

close 关闭套接字

在Linux系统下,close函数用于关闭文件,我们知道实际上在网络中通信其实也是对文件进行操作,所以通信结束后我们需要关闭套接字,其函数原型如下:

cpp 复制代码
int close(int fd);

参数:fd:是要关闭的套接字描述符(也就是由socket函数创建的套接字描述符)。

返回值:如果关闭成功,返回0;如果失败,返回 - 1,并设置errno来指示错误类型。

案例:echosever

简单的回显服务器和客户端代码
makefile

bash 复制代码
.PHONY:all
all:server client
server:UdpServermain.cc
	g++ -o $@ $^ -std=c++17
client:UdpClientmain.cc
	g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
	rm -f server client

UdpServer.hpp

cpp 复制代码
#include "common.hpp"

const uint16_t default_port = 8080;
class UdpServer
{
public:
    UdpServer(uint16_t port = default_port)
        : _port(port), _sockfd(-1)
    {
        // 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            cout << "create socket error" << endl;
            exit(SOCKET_ERROR);
        }
        // 将socket绑定到ip和端口
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());
        //云服务器不允许直接 bind 公有IP,我们也不推荐编写服务器的时候,bind 明确的 IP,推荐直接写成 INADDR_ANY
        //在网络编程中,当一个进程需要绑定一个网络端口以进行通信时,可以使用INADDR_ANY 作为 IP 地址参数。
        //这样做意味着该端口可以接受来自任何 IP 地址的连接请求,无论是本地主机还是远程主机。例如,如果服务
        //器有多个网卡(每个网卡上有不同的 IP 地址),使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个
        //网卡/IP 地址上面获取的。
        local.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            cout << "bind socket error" << endl;
            exit(BIND_ERROR);
        }
    }
    ~UdpServer()
    {
        if (_sockfd > 0)
            close(_sockfd);
        _sockfd = -1;
        cout << "socket closed" << endl;
    }
    void start()
    {
        // 循环接收数据
        while (true)
        {
            struct sockaddr_in from;
            socklen_t len = sizeof(from);
            char buf[1024];
            int n = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&from, &len);
            if (n > 0)
            {
                buf[n] = 0;
                string ip = inet_ntoa(from.sin_addr);
                int port = ntohs(from.sin_port);
                cout << "receive from [" << ip << ":" << port << "]#" << buf << endl;
                sendto(_sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from, len);
            }
        }
    }

private:
    uint16_t _port;
    int _sockfd;
};

UdpServermain.cc

cpp 复制代码
#include "UdpServer.hpp"
#include <iostream>
using namespace std;
//./server localport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Usage:./server localport" << endl;
        return Usage_ERROR;
    }
    int port = stoi(argv[1]);
    UdpServer server(port);
    server.start();
    return 0;
}

UdpClientmain.cc

cpp 复制代码
#include "common.hpp"
//./client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "Usage: " << argv[0] << " sever_ip sever_port" << endl;
        return Usage_ERROR;
    }
    // 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "create socket error" << endl;
        exit(SOCKET_ERROR);
    }
    // 填充一下 server 信息
    string serverip = argv[1];
    int serverport = stoi(argv[2]);
    struct sockaddr_in serveraddr;
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(serverport);
    serveraddr.sin_addr.s_addr = inet_addr(serverip.c_str());
    // client 要不要进行 bind? 一定要 bind 的!!
    // 但是不需要显示 bind,client 会在首次发送数据的时候会自动进行bind
    // 为什么?server 端的端口号,一定是众所周知,不可改变的,client非常多,需要 bind 随机端口.
    while (true)
    {
        cout << "please input message:";
        string message;
        getline(cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
        char buf[1024];
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        int n = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&tmp, &len);
        if (n > 0)
        {
            buf[n] = 0;
            cout << "server say:" << buf << endl;
        }
        else
            break;
    }
    return 0;
}

以上为Linux版本,Windows版本如下:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
using namespace std;
string serverip = "110.41.138.70";// 填写云服务器ip
int serverport = 8080;// 填写云服务开放的端口
int main( )
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);
	//创建socket
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd < 0)
	{
		cout << "create socket error" << endl;
		return 1;
	}
	//填充server信息
	struct sockaddr_in serveraddr;
	memset(&serveraddr, sizeof(serveraddr),0);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(serverport);
    serveraddr.sin_addr.s_addr = inet_addr(serverip.c_str());
	
    while (true)
    {
        cout << "please input message:";
        string message;
        getline(cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
        char buf[1024];
        struct sockaddr_in tmp;
        int len = sizeof(tmp);
        int n = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&tmp, &len);
        if (n > 0)
        {
            buf[n] = 0;
            cout << "server say:" << buf << endl;
        }
    }
    closesocket(sockfd);
    WSACleanup();
	return 0;
}

WinSock2.h 是 Windows Sockets API (应用程序接口)的头文件,用于在 Windows 平台上进行网络编程。它包含了 Windows Sockets 2 ( Winsock2 )所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字(sockets )进行网络通信。
在编写使用 Winsock2 的程序时,需要在源文件中包含 WinSock2.h 头文件。这样,编译器就能够识别并理解 Winsock2 中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。此外,与 WinSock2.h 头文件相对应的是 ws2_32.lib 库文件。在链接阶段,需要将这个库文件链接到程序中,以确保运行时能够找到并调用 Winsock2 API 中实现的函数。
在 WinSock2.h 中定义了一些重要的数据类型和函数,如:

  • WSADATA:保存初始化 Winsock 库时返回的信息。
  • SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。
  • sockaddr_in:IPv4 地址结构体,用于存储 IP 地址和端口号等信息。
  • socket():创建一个新的套接字。
  • bind():将套接字与本地地址绑定。
  • listen():将套接字设置为监听模式,等待客户端的连接请求。
  • accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端进行通信。
    WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化 Winsock 库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首先执行,它扮演着初始化的角色。
    以下是 WSAStartup 函数的一些关键点:
    它接受两个参数: wVersionRequested 和 lpWSAData 。 wVersionRequested 用于指定所请求的 Winsock 版本,通常使用 MAKEWORD(major, minor) 宏,其中 major 和 minor 分别表示请求的主版本号和次版本号。 lpWSAData 是一个指向 WSADATA 结构的指针,用于接收初始化信息。
    如果函数调用成功,它会返回 0 ;否则,返回错误代码。
    WSAStartup 函数的主要作用是向操作系统说明我们将使用哪个版本的 Winsock 库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock 库的状态会被初始化,应用程序就可以使用 Winsock 提供的一系列套接字服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程序可以与底层的网络协议栈进行交互,实现网络通信。在调用 WSAStartup 函数后,如果应用程序完成了对请求的 Socket 库的使用,应调用WSACleanup 函数来解除与 Socket 库的绑定并释放所占用的系统资源。
相关推荐
勤奋的凯尔森同学2 小时前
webmin配置终端显示样式,模仿UbuntuDesktop终端
linux·运维·服务器·ubuntu·webmin
丁卯4043 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo3 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
人间打气筒(Ada)4 小时前
MySQL主从架构
服务器·数据库·mysql
落笔画忧愁e5 小时前
FastGPT快速将消息发送至飞书
服务器·数据库·飞书
小冷爱学习!5 小时前
华为动态路由-OSPF-完全末梢区域
服务器·网络·华为
技术小齐6 小时前
网络运维学习笔记 016网工初级(HCIA-Datacom与CCNA-EI)PPP点对点协议和PPPoE以太网上的点对点协议(此处只讲华为)
运维·网络·学习
ITPUB-微风6 小时前
Service Mesh在爱奇艺的落地实践:架构、运维与扩展
运维·架构·service_mesh
落幕6 小时前
C语言-进程
linux·运维·服务器
chenbin5207 小时前
Jenkins 自动构建Job
运维·jenkins