套接字编程简介

GuangYing Lv1

套接字地址结构

IPv4套接字地址结构

网际套接字地址结构
sockaddr_in命名,定义在<netinet/in.h>头文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct in_addr {
in_addr_t s_addr;
};

struct sockaddr_in {
unit8_t sin_len;
//长度字段sin_len
//为支持OSI协议随4.3BSD-Reno添加的
//POSIX规范不要求有这个成员
//有了长度字段才简化了长度可变套接字地址结构的处理
//无需设置和检查,由内核使用

sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
//POSIX规范只需要以上三个字段

char sin_zero[8];
//几乎所有的实现都增加了此字段
//所以所有套接字地址结构大小都至少为16字节
//总把该字段置为0
//当捆绑一个非通配的IPv4地址时该字段必须为0
};

套接字地址结构仅在给定主机上使用

  • 虽然结构总的某些字段(如IP地址和端口号)用在不同主机之前的通信中
  • 但结构本身并不在主机之间传递

通用套接字地址结构

套接字地址结构总以引用形式传递
而使用这种参数的函数必须能处理任何协议族的套接字地址结构

ANSI C后可用void* 通用指针解决
但套接字函数在ANSI C之前定义
因此在<sys/socket.h>头文件中定义一个通用的套接字地址结构

1
2
3
4
5
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
}

套接字函数被定义为指向某个通用套接字地址结构的一个指针作为参数之一
如:int bind(int, struct sockaddr *, socklen_t);
因此任何调用都必须将特定协议的套接字地址结构的指针进行类型强制转换,变为通用套接字地址结构指针
如:struct sockaddr_in serv;
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));

IPv6套接字地址结构

<netinet/in.h>头文件中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct in6_addr {
unit8_t s6_addr[16];
};

#define SIN6_LEN

struct sockaddr_in6 {
unit8_t sin6_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint_32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
  • 若系统支持套接字地址结构中的长度字段,则SIN6_LEN常值必须定义
  • IPv6的地址族是AF_INET6,而IPv4的地址族是AF_INET
  • 结构中字段的先后顺序做过编排,使得如果sockaddr_in6结构本身是64位对齐的,那么128位的sin6_addr字段也是64位对齐的
  • sin6_flowinfo字段分成两个字段:
    • 低序20位是流标
    • 高序12位保留
  • 对于具备范围的地址,sin6_scope_id字段标识其范围,最常见的是链路局部地址的接口索引

新的通用套接字地址结构

作为IPv6套接字API的一部分定义的新通用套接字地址结构
克服了现有的一些缺点
足以容纳系统所支持的任何套接字地址结构
<netinet/in.h>头文件中定义

1
2
3
4
struct sockaddr_storage {
uint8_t ss_len;
sa_family_t ss_family;
};

相比之下sockaddr存在以下两点差别:

  • sockaddr_storage能够满足最苛刻的对齐要求
  • sockaddr_storage足够大,能够容纳系统支持的任何套接字地址结构

注:出ss_family和ss_len外,sockaddr_storage结构总的其他字段对用户来说是透明的
sockaddr_storage结构必须类型强制转换成或复制到适合ss_family字段所给出地址类型的套接字地址结构中,才能访问其他字段

值-结果参数

函数被调用时,结构大小是一个,告诉内核该结构的大小,这样内核在写该结构时不至于越界
当函数返回时,结构大小又是一个结果,告诉进程内核在该结构中究竟存储了多少信息

这种类型的参数称为值-结果参数

字节排序函数

内存中存储两个字节的方法有两种:

  • 将低序字节存储在起始地址:小端字节序
  • 将高序字节存储在起始地址:大端字节序

网际协议使用大端字节序来传送这些多字节整数

主机字节序和网络字节序之间转换使用以下4个函数:

1
2
3
4
5
6
7
8
9
10
11
#include <netinet/in.h>

uint16_t htons(uint16_t host16bitvalue);

uint32_t htonl(uint32_t host32bitvalue);
均返回:网络字节序的值

uint16_t ntohs(uint16_t host16bitvalue);

uint32_t ntohl(uint32_t host32bitvalue);
均返回:主机字节序的值

h代表host
n代表network
s代表short 现在为一个16为的值(端口号)
l代表long 现在为一个32位的值(IPv4地址)

使用时不关心主机字节序和网络字节序的真是值
与网际协议所用字节序相同的系统中,这四个函数通常为空宏

字节(byte)
大多因特网标准使用八位组表示一个8位的量

字节操纵函数

以b(表示字节)开头的函数起源于4.2BSD,支持套接字函数的系统仍然提供它们
mem(表示内存)开头的函数起源于ANSI C标准,支持ANSI C函数库的所有系统都提供它们

源自Berkeley的函数

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

void bzero(void *dest, size_t nbytes);

void bcopy(const void *src, void *dest, size_t nbytes);

int bcmp(const void *ptrl, const void *ptr2, size_t nbytes);
返回:若相等则为0,否则为非0

ANSI C函数:

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

void *memset(void *dest, int c, size_t len);

void *memcpy(void *dest, const void *src, size_t bytes);

int memcmp(const void *ptrl, const void *ptr2, size_t nbytes);
返回:若相等则为0,否则为<0或>0

当源字节串与目标字节串重叠时,bcopy能正确处理,但memcpy的结果不确定
此时必须改用ANSI C的memmove函数

memcpy中指针参数顺序与C中复制语句顺序相同:
dest = src;
ANSI C的memXXX函数都需要一个长度参数,且总是最后一个参数

inet_aton、inet_addr、inet_ntoa函数

在点分十进制数串与32位网络字节序二进制之间转换IPv4地址

较新的函数inet_ptoninet_ntop对IPv4和IPv6地址都适用

1
2
3
4
5
6
7
8
9
10
11
12
#include <arpa/inet.h>

int inet_aton(const char *strptr, struct in_addr *addrptr);
返回:若字符串有效则为1,否则为0

in_addr_t inet_addr(const char *strptr);
返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE
对于255.255.255.255(IPv4的优先广播地址)不能由该函数处理
因为其二进制数被用来指示该函数失败

char *inet_ntoa(struct in_addr inaddr);
返回:指向一个点分十进制数串的指针

函数以结构为参数是罕见的,更常见的是以指向结构的指针作为参数

inet_pton、inet_ntop函数

随IPv6出现的新函数,对IPv4和IPv6地址都适用

p:表达
n:数值

地址表达格式通常为ASCII字符串
数值格式则是存放到套接字地址结构中的二进制值

1
2
3
4
5
6
7
#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);
返回:若成功则为1,若输入不是有效的表达格式则为0,若出错则为-1

const char *inet_ntop(int family, const void *addrptr, char *strpr, size_t len);
返回:若成功则为指向结果的指针,若出错则为NULL

这两个函数的family参数既可以是AF_INET也可以是AF_INET6
若以不被支持的地址族作为参数,则都返回一个错误,并将errno置为EAFNOSUPPORT

头文件<netinet/in.h>中有定义:

1
2
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

如果len太小,不够容纳结果(包括结尾的空字符),则返回一个空指针,置errno为ENOSPC

inet_ntop函数的strptr参数不可为空指针,必须自己分配内存并指定大小

示例:
旧:foo.sin_addr.s_addr = inet_addr(cp);
新:inet_pton(AF_INET, cp, &foo.sin_addr);

旧:ptr = inet_ntoa(foo.sin_addr);
新:char str[INET_ADDRSTRLEN];
ptr = inet_ntop(AF_INET, &foo.sin_addr, str, sizeof(str));

sock_ntop和相关函数

inet_ntop需要调用者知道套接字地址结构的格式和地址族
这样代码就与协议相关了

为解决此问题,可自行编写名为sock_ntop的函数

  • 以指向某个套接字地址结构的指针为参数
  • 查看结构的内部
  • 然后调用适当的函数返回该地址的表达格式
    1
    2
    3
    4
    #include "unp.h"

    char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen);
    返回:若成功则为非空指针,出错则为NULL

注:对结果进行静态存储导致该函数不可重入且非线程安全

  • 标题: 套接字编程简介
  • 作者: GuangYing
  • 创建于 : 2024-12-24 22:19:16
  • 更新于 : 2024-12-24 22:23:51
  • 链接: http://quebo.cn/2024/12/24/套接字编程简介/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。