网络编程基础:字节序、IP地址转换、sockaddr结构与套接字API详解

socket编程

1. 字节序

在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的
存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。

  • 主机字节序 (小端)
    数据的低位字节存储在低地址位,高位字节存储在高地址位。
  • 网络字节序 (大端)
    数据的高位字节存储在低地址位,低位字节存储在高地址位。

套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。

// 有一个16进制的数, 有32位 (int): 0xab5c01ff
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制) 
                 内存低地址位                内存的高地址位
--------------------------------------------------------------------------->
小端:         0xff        0x01        0x5c        0xab
大端:         0xab        0x5c        0x01        0xff

从主机字节序到网络字节序的转换htons(16位),htonl(32位)
从网络字节序到主机字节序的转换ntohs,ntohl
htonl() 函数用于将32位(即4字节)的整数从主机字节序转换为网络字节序。函数原型如下:

uint32_t htonl(uint32_t hostlong);
  • hostlong:一个 uint32_t 类型的变量,表示在主机字节序下的32位整数值。
  • 返回值:
    该函数返回一个 uint32_t 类型的值,表示输入值转换为网络字节序后的32位整数值。

2. IP地址转换

IP地址在网络通信中通常以32位整数的形式表示,并且在传输时使用网络字节序(大端字节序)。当在主机上操作IP地址时,可能需要将其从主机字节序转换为网络字节序,或者反之。

IPv4地址由四个字节组成,每个字节通常以点分十进制形式表示,如 192.168.1.1。在内存中,这实际上是一个32位的无符号整数
从主机字节序到网络字节序:

// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst); 

参数:

  • af: 地址族(IP地址的家族包括ipv4和ipv6)协议
    AF_INET: ipv4格式的ip地址
    AF_INET6: ipv6格式的ip地址
  • src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.1
  • dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
  • 返回值:成功返回1,失败返回0或者-1

** 从网络字节序到主机字节序:**

#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址        
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:

  • af: 地址族协议
    AF_INET: ipv4格式的ip地址
    AF_INET6: ipv6格式的ip地址
  • src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
  • dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址
  • size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
  • 返回值:
    成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
    失败: NULL

还有一组函数也能进程IP地址大小端的转换,但是只能处理ipv4的ip地址:

// 点分十进制IP -> 大端整形
in_addr_t inet_addr (const char *cp);

// 大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);

3. sockaddr 数据结构

sockaddr 是一个在Unix和类Unix系统(包括Linux)中用于表示套接字地址的通用数据结构。它被设计为一个抽象层,允许应用程序通过同一接口处理不同类型的网络协议和地址族。sockaddr 结构体定义在 <sys/socket.h> 头文件中,其基本定义如下:

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

sockaddr 通常与更具体的结构体结合使用,如 sockaddr_in 对于 IPv4 和sockaddr_in6对于 IPv6。

  • sockaddr_in (IPv4)
struct sockaddr_in {
    sa_family_t sin_family; /* 地址家族,AF_INET */
    in_port_t   sin_port;   /* TCP或UDP端口号 */
    struct in_addr sin_addr; /* 32位IPv4地址 */
};
  • sockaddr_in6 (IPv6)
    对于IPv6地址,使用sockaddr_in6结构体:
struct sockaddr_in6 {
    sa_family_t sin6_family; /* 地址家族,AF_INET6 */
    in_port_t   sin6_port;   /* TCP或UDP端口号 */
    uint32_t    sin6_flowinfo; /* 流量信息字段 */
    struct in6_addr sin6_addr; /* 128位IPv6地址 */
    uint32_t    sin6_scope_id; /* 范围ID */
};

使用示例:

#include <sys/socket.h>
#include <netinet/in.h>

struct sockaddr_in serv_addr;

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345); // 服务器的端口号
serv_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // 服务器的IP地址

// 绑定或连接套接字到这个地址

当从函数如 getpeername() getsockname()获取地址信息时,通常需要传递一个指向 sockaddr 指针的指针和一个指向 socklen_t 的指针来存储结构体的实际大小。这是因为不同的地址类型可能具有不同的大小。

4. 套接字函数

1. socket():创建套接字

int socket(int domain, int type, int protocol);

参数:

  • domain: 使用的地址族协议
    AF_INET: 使用IPv4格式的ip地址
    AF_INET6: 使用IPv4格式的ip地址
  • type:
    SOCK_STREAM: 使用流式的传输协议
    SOCK_DGRAM: 使用报式(报文)的传输协议
  • protocol: 一般写0即可, 使用默认的协议
    SOCK_STREAM: 流式传输默认使用的是tcp
    SOCK_DGRAM: 报式传输默认使用的udp
    返回值:
  • 成功: 可用于套接字通信的文件描述符,失败: -1

函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。
2. bind():绑定地址到套接字

// 将文件描述符和本地的IP与端口进行绑定   
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd: 监听的文件描述符, 通过socket()调用得到的返回值
  • addr: 传入参数, 要绑定的IP和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
  • addrlen: 参数addr指向的内存大小, sizeof(struct sockaddr)
  • 返回值:成功返回0,失败返回-1
    3. listen():监听套接字
    // 给监听的套接字设置监听
    int listen(int sockfd, int backlog);

参数:

  • sockfd: 文件描述符, 可以通过调用socket()得到,在监听之前必须要绑定 bind()
  • backlog: 同时能处理的最大连接要求,最大值为128
  • 返回值:函数调用成功返回0,调用失败返回 -1
    4. accept():接受连接请求。
    // 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)		
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd: 监听的文件描述符
  • addr: 传出参数, 里边存储了建立连接的客户端的地址信息
  • addrlen: 传入传出参数,用于存储addr指向的内存大小
  • 返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1

这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。

5. send()/recv():发送/接收数据

read()

ssize_t read(int filedes, void *buf, size_t nbyte);
  • filedes: 文件描述符,标识正在读取的文件或设备。
  • buf: 指向缓冲区的指针,数据将被读入此缓冲区。
  • nbyte: 指定要尝试从文件描述符读取的字节数。
    write()
ssize_t write(int filedes, const void *buf, size_t nbyte);
  • filedes: 文件描述符,标识正在写入的文件或设备。
  • buf: 指向包含要写入数据的缓冲区的指针。
  • nbyte: 指定要尝试写入的字节数。
    recv()
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd: 套接字描述符,标识正在接收数据的套接字。
  • buf: 指向缓冲区的指针,接收的数据将被存储在此缓冲区中。
  • len: 指定缓冲区的大小,也是尝试接收的最大字节数。
  • flags: 可选标志,用于修改接收行为(如非阻塞接收或带外数据接收)。
    send()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd: 套接字描述符,标识正在发送数据的套接字。
  • buf: 指向包含要发送数据的缓冲区的指针。
  • len: 指定要尝试发送的字节数。
  • flags: 可选标志,用于修改发送行为(如紧急数据或等待所有数据被发送)

这些函数在成功时返回实际读取或写入的字节数,在出错时返回 -1。对于 recv() 和 send(),当没有数据可读或对端关闭连接时,recv() 可能返回0。
6. connect():主动发起连接

    // 成功连接服务器之后, 客户端会自动随机绑定一个端口
    // 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd: 通信的文件描述符, 通过调用socket()函数就得到了
  • addr: 存储了要连接的服务器端的地址信息: iP 和 端口,这个IP和端口也需要转换为大端然后再赋值
  • addrlen: addr指针指向的内存的大小 sizeof(struct sockaddr)
  • 返回值:连接成功返回0,连接失败返回-1
  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值