套接字、协议族与地址族

Posted by KalosAner on January 11, 2025

一、套接字

无论在 Linux 上还是 Windows 上,创建套接字时都会同时 I/O 缓存区,并且每个套接字都有独立的 I/O 缓存区。关闭套接字会继续传输输出缓存区中的数据,但是会丢失输入缓存区中的数据。

Snipaste_2025-01-16_16-09-11

Linux

Linux 下使用 socket 函数创建一个套接字,该函数定义在 sys/socket.h 头文件下,调用成功返回一个套接字描述符。Linux 有一个哲学就是“万物皆文件”,在 Linux 下套接字描述符和文件描述符是一样的。

该函数原型如下:

1
2
3
4
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 调用成功返回套接字描述符,失败返回 -1

第一个参数 domain 传入套接字使用的协议族,主要有以下选择

名称 协议族
PF_INET IPv4 互联网协议族
PF_INET6 IPv6 互联网协议族
PF_LOCAL 本地通信的 UNIX 协议族
PFF_PACKET 底层套接字的协议族
PF_IPX IPXNovell协议族

第二个参数 type 传入套接字数据传输类型信息,有两种选择

  1. 面向连接的套接字 SOCK_STREAM
  2. 面向消息的套接字 SOCK_DREAM

第三个参数 protocol 用来最终决定使用的协议,一般传入 0,只有出现 “同一个协议族中存在多个数据传输方式相同的不同协议”时才需要使用。

Windows

Windows 下的套接字形式与 Linux 下的套接字有所不同,也不能像文件那样操作,如下是 Windows 下的套接字原型:

1
2
3
4
#include <winsock2.h>

SOCKET socket(int af, int type, int protocol);
// 成功返回 socket 句柄,失败返回 INVALID_SOCKET

Windows 套接字函数的三个参数与 Linux 下是一样的,返回值类型 SOCKET 本质上也是一个 int,但是还是建议使用 SOCKET 接收套接字句柄。 在 Windows 上进行 Winsock 编程时,首先必须调用 WSAStartup 函数,结束时调用 WSACleanup,函数原型如下。

1
2
3
4
5
6
7
#include <winsock2.h>

// 成功返回 0,失败返回非零的错误代码值
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

//成功返回 0,失败返回 SOCKET_ERROR
int WSACleanup(void);

其中 wVersionRequested 参数表示要使用的 Winsock 版本信息,lpWSAData 表示 WSADATA 结构体变量的地址值。 示例:

1
2
3
4
5
6
7
8
9
10
11
12
WSADATA	wsaData;
SOCKET Sock;

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	ErrorHandling("WSAStartup() error!");

Sock = socket(PF_INET, SOCK_STREAM, 0);
if (Sock == INVALID_SOCKET)
	ErrorHandling("socket() error");

closesocket(Sock);
WSACleanup();

MAKEWORD(2, 2) 表示主版本为 2,副版本为 2,返回 0x0202 MAKEWORD(1, 2) 表示主版本为 1,副版本为 2,返回 0x0201

二、地址族

在进行网络编程时,每个套接字需要绑定一个 IP 地址和一个 port 作为标识符进行通信。服务端使用 bind 函数进行绑定,bind 可以指定 IP 地址和 port。客户端可以通过 connect 函数(面向连接)或者 sendto 函数(面向消息)进行绑定,IP 地址默认绑定本机 IP,port 随机。 在通信时,绑定、连接和发送都需要传入目标的地址信息,一般传入 struct sockaddr 类型的数据,原型如下。

1
2
3
4
struct sockaddr {
	sa_family_t sin_family;  // 地址族,类似于协议族
	char sa_data[14];        // 地址信息,适用 IPv4 和 IPv6
}

但是 IP 和 port 信息一般都需要进行转换才能使用,struct sockaddr 转换时很不方便,所以一般都先转换成 struct sockaddr_in 类型,再强转成 struct sockaddrstruct sockaddr_in 只适用于 IPv4,该结构体原型如下。

1
2
3
4
5
6
7
8
9
struct sockaddr_in {
	sa_family_t sin_family;		// 地址族
	uint16_t sin_port;			// 16位 port
	struct in_addr sin_addr;	// 32 位 IPv4 地址
	char sin_zero[8];			// 不使用
}
struct in_addr {
	in_addr_t s_addr;    // 32 位 IPv4 地址
}

成员 sin_family

名称 协议族
AF_INET IPv4 互联网地址族
AF_INET6 IPv6 互联网地址族
AF_LOCAL 本地通信的 UNIX 地址族

成员 sin_zero 无特殊含义,只是为了使结构体 sockaddr_in 的大小与 sockaddr 保持一致而插入的成员,必须填充为 0,否则结果不可预期。

三、绑定函数

bind 函数原型如下。

1
2
3
4
5
6
7
8
9
// linux
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen addrlen);

// windows
#include <winsock2.h>

int bind(SOCKET s, const struct sockaddr *name, int namelen);

示例 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// linux
struct sockaddr_in serv_addr;

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));

if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
	error_handling("bind() error"); 

// windows
SOCKADDR_IN servAddr;

memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));

if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	ErrorHandling("bind() error");

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//linux
struct sockaddr_in serv_addr;

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
// serv_addr.sin_addr.s_addr=inet_addr(argv[1]); // 提示被弃用,推荐用 inet_pton
if (inet_pton(AF_INET, argv[1], &(serv_addr.sin_addr)) != 1) {
	printf("Invalid IP address\n");
	return 1;
}
serv_addr.sin_port=htons(atoi(argv[2]));
	
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 
	error_handling("connect() error!");

//windows
SOCKADDR_IN servAddr;

memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
//servAddr.sin_addr.s_addr = inet_addr(argv[1]);
if (inet_pton(AF_INET, argv[1], &(servAddr.sin_addr)) != 1) {
	printf("Invalid IP address\n");
	return 1;
}
servAddr.sin_port = htons(atoi(argv[2]));

if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	ErrorHandling("connect() error!");

inet_pton:将 IP 地址写入结构体 servAddr 中 将主机字节序转换成网络字节序 uint32_t htonl(uint32_t hostlong) uint16_t htons(uint16_t hostshort) 将网络字节序转换成主机字节序 uint32_t ntohl(uint32_t netlong) uint16_t ntohs(uint16_t netshort) 在客户端中没有使用 bind 绑定 IP 地址和 port,所以在调用 connect 时会自动给套接字绑定,默认 IP 地址为本地 IP 地址,port 为随机 port。

四、函数原型

Linux

1
2
3
4
5
6
7
8
9
int inet_pton(int af, const char *src, void *dst);

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);
1
2
3
#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明

  • af:地址族,支持 AF_INET(IPv4)和 AF_INET6(IPv6)。
  • src:指向存储网络字节序地址的源地址(struct in_addr *struct in6_addr *)。
  • dst:指向存储结果字符串的目标缓冲区。
  • size:目标缓冲区的大小。

返回值

  • 成功:返回 dst 的指针。
  • 失败:返回 NULL,并设置 errno

Windows

1
2
3
4
5
6
7
8
9
int inet_pton(int af, const char *src, void *dst);

u_long htonl(u_long hostlong);

u_short htons(u_short hostshort);

u_long ntohl(u_long netlong);

u_short ntohs(u_short netshort);
1
2
3
#include <ws2tcpip.h>

PCSTR WSAAPI inet_ntop(int af, const void *src, PSTR dst, size_t size);

参数说明

  • af:地址族,支持 AF_INETAF_INET6
  • src:指向存储网络字节序地址的源地址(struct in_addr *struct in6_addr *)。
  • dst:指向存储结果字符串的目标缓冲区。
  • size:目标缓冲区的大小。

返回值

  • 成功:返回 dst 的指针。
  • 失败:返回 NULL,并通过 WSAGetLastError 获取错误代码。**