【Linux Server】四、Linux网络编程

四、Linux网络编程

1.以太网的MTU是1500字节,因此过长的IP数据报可能需要被分片传输。
帧才是最终在物理网络上传送的字节序列。

2.所有知名应用层协议使用的端口号都可在/etc/services文件中找到。

3.Linux下可以使用arp命令来查看和修改ARP高速缓存。

sudo arp -a                                         # 查看当前时刻的ARP缓存内容
sudo arp -d 192.168.1.109                           # 删除192.168.1.109对应的ARP缓存项
sudo arp -s 192.168.1.109 08:00:27:53:10:67         # 添加192.168.1.109对应的ARP缓存项

ARP通信在TCP连接建立之前就已经完成。

4.Linux使用/etc/resolv.conf文件来存放DNS服务器的IP地址。
host命令使用DNS协议和DNS服务器通信,其-t选项告诉DNS协议使用哪种查询类型。下面使用A类型,即通过机器的域名获得其IP地址(但实际上返回的资源记录中还包含机器的别名)。

host -t A www.baidu.com

5.IPv4协议的头部长度通常为20字节,最长为60字节。

6.路由器都能执行数据报的转发操作,而主机一般只发送和接收数据报,这是因为主机上/proc/sys/net/ipv4/ip_forward内核参数默认被设置为0。我们可以通过修改它来使能主机的数据报转发功能。

7.在TCP/IP四层模型中,只有应用层是在用户空间的,其他都是在内核空间的。

8.字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

9.判断当前主机的字节序

#include <stdio.h>
int main() 
{
    union 
    {
        short value;    // 2字节
        char bytes[sizeof(short)];  // char[2]
    } test;

    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) 
        printf("大端字节序\n");
    else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) 
        printf("小端字节序\n");
    else 
        printf("未知\n");
    return 0;
}

10.网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。

// h:host-主机;n:network-网络;s:short-unsigned short;l:long-unsigned int
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort);  // 网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong);  // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong);   // 网络字节序 - 主机字节序

11.socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

// 通用的 socket 地址结构体
#include <bits/socket.h>
struct sockaddr 
{
    sa_family_t sa_family;  // sa_family 成员是地址族类型(sa_family_t)的变量。
    char sa_data[14];       // sa_data 成员用于存放 socket 地址值。
};
typedef unsigned short int sa_family_t;

// 下面是新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
例如 TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6。
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

但是日常开发中,我们一般不会使用通用的 socket 地址结构体。

// UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
    sa_family_t sin_family;
    char sun_path[108];
};

// TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
#include <netinet/in.h>
struct sockaddr_in
{
    sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
    in_port_t sin_port; /* Port number. */
    struct in_addr sin_addr; /* Internet address. */
    /* Pad to size of `struct sockaddr'. */
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)];
};

struct in_addr
{
    in_addr_t s_addr;
};

struct sockaddr_in6
{
    sa_family_t sin6_family;
    in_port_t sin6_port; /* Transport layer port # */
    uint32_t sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr; /* IPv6 address */
    uint32_t sin6_scope_id; /* IPv6 scope-id */
};

typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

12.下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

// 旧的3个转换函数,仅适用于IPv4,不推荐使用.
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);     // 不可重入

// 下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
#include <arpa/inet.h>

// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
    - af:地址族: AF_INET AF_INET6
    - src:需要转换的点分十进制的IP字符串
    - dst:转换后的结果保存在这个里面
    
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    - af:地址族: AF_INET AF_INET6
    - src: 要转换的ip的整数的地址
    - dst: 转换成IP地址字符串保存的地方
    - size:第三个参数的大小(数组的大小)
        下面两个宏能帮助我们指定这个大小(分别用于IPv4和Ipv6):
        #include <netinet/in.h>
        #define INET_ADDRSTRLEN 16
        #define INET6_ADDRSTRLEN 46
    - 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

13.套接字函数

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略

int socket(int domain, int type, int protocol);
    - 功能:创建一个套接字
    - domain: 协议族
        AF_INET : ipv4
        AF_INET6 : ipv6
        AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
    - type: 通信过程中使用的协议类型
        SOCK_STREAM : 流式协议
        SOCK_DGRAM : 报式协议
    - protocol : 具体的一个协议。一般写0
    - SOCK_STREAM : 流式协议默认使用 TCP
    - SOCK_DGRAM : 报式协议默认使用 UDP
    - 返回值:
        - 成功:返回文件描述符,操作的就是内核缓冲区。
        - 失败:-1
    - 自Linux内核版本2.6.17起,type参数可以接受上述服务类型(SOCK_STREAM或SOCK_DGRAM)与下面两个重要的标记相与的值:SOCK_NONBLOCK和SOCK_CLOEXEC。它们分别表示将新创建的socket设置为非阻塞的,以及用fork调用创建子进程时在子进程中关闭该socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
    - 功能:绑定,将 sockfd 和本地的IP + 端口进行绑定
    - sockfd : 通过socket函数得到的文件描述符
    - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
    - addrlen : 第二个参数结构体占的内存大小
    - 返回值:失败返回-1并设置errno。其中两种常见的errno是EACESS和EADDRINUSE。
        EACESS表示被绑定是地址是受保护的地址,仅超级用户能够访问。
        EADDRINUSE表示被绑定的地址正在使用中。

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
    - 功能:监听这个socket上的连接
    - sockfd : 通过socket()函数得到的文件描述符
    - backlog : 半连接的和完全连接的socket的和的最大值, 一般指定5
        在内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    - 功能:接收客户端连接,默认是阻塞的,阻塞等待客户端连接
    - sockfd : 用于监听的文件描述符
    - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
    - addrlen : 指定第二个参数的对应的内存大小
    - 返回值:
        - 成功 :用于通信的文件描述符
        - 失败 : -1
    - accpet只是在监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    - 功能: 客户端连接服务器
    - sockfd : 用于通信的文件描述符
    - addr : 客户端要连接的服务器的地址信息
    - addrlen : 第二个参数的内存大小
    - 返回值:成功 0, 失败 -1。其中两种常见的errno是ECONNREFUSED和ETIMEOUT。
        ECONNREFUSED表示目标端口不存在,连接被拒绝。
        ETIMEOUT表示连接超时。

ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

14.TCP协议

  • 16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。

  • 32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从 B 到 A)的 TCP 报文段的序号值也具有相同的含义。

  • 32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。

  • 4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示15,所以 TCP 头部最长是60 字节。

  • 6 位标志位包含如下几项:

    • URG 标志,表示紧急指针(urgent pointer)是否有效。
    • ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
    • PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
    • RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
    • SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
    • FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
  • 16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

  • 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。

  • 16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。

15.TCP三次握手

三次握手的目的是保证双方互相之间建立了连接。(保证自己和对方的收发都是没有问题的)
三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
SYN=1的报文是不能携带数据的,也就是前两次握手是带不了数据的。

  • 第一次握手:
    • 客户端将SYN标志位置为1。
    • 生成一个随机的32位的序号seq=J。
  • 第二次握手:
    • 服务器端接收客户端的连接: ACK=1
    • 服务器会回发一个确认序号: ack=客户端的序号J + 1
    • 服务器端会向客户端发起连接请求: SYN=1
    • 服务器会生成一个随机序号:seq = K
  • 第三次握手:
    • 客户单应答服务器的连接请求:ACK=1
    • 客户端回复收到了服务器端的数据:ack=服务端的序号K + 1
  1. TCP四次挥手

四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。
客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。
因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。

  • 第一次挥手(客户端调用close())

    • 客户端将FIN标志位置为1
    • 发送seq=客户端的序号J+1,ack=服务端的序号K+1
  • 第二次挥手

    • 服务端发送ack=客户端的序号J+2
  • 第三次挥手(服务端调用close())

    • 服务端将FIN标志位置为1
    • 服务端发送seq=服务端的序号K+1
  • 第四次挥手

    • 客户端发送ack=服务端的序号K+2
    • 客户端发送完就马上进入TIME_WAIT阶段,时间长度为2MSL。
  • 2MSL(Maximum Segment Lifetime,最大报文生存时间)

    • 主动断开连接的一方, 最后进入一个 TIME_WAIT状态, 这个状态会持续2msl
    • msl: 官方建议: 2分钟, 实际是30s
      当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。
      这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
  • 半关闭

    • 当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
  • 从程序的角度,可以使用 API 来控制实现半连接状态:

#include <sys/socket.h>
int shutdown(int sockfd, int how);
    - sockfd: 需要关闭的socket的描述符
    - how: 允许为shutdown操作选择以下几种方式:
        SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
        SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去。这种情况下,连接处于半关闭状态。
        SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
  • 使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
  • shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
  • 注意:
    • 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
    • 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。但如果一个进程 close(sfd) 将不会影响到其它进程。

17.端口复用

端口复用最常用的用途是:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口

#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    参数:
    - sockfd : 要操作的文件描述符
    - level : 级别 - SOL_SOCKET (端口复用的级别)
    - optname : 选项的名称
        SO_REUSEADDR
        SO_REUSEPORT
    - optval : 端口复用的值(整形)
        1 : 可以复用
        0 : 不可以复用
    - optlen : optval参数的大小

// 端口复用,设置的时机是在服务器绑定端口之前。
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval);
bind();

此外,我们也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT状态,进而允许应用程序立即重用本地的socket地址。

18.TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一旦发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃。应该程序可以使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。

如果客户端(或服务端)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。

19.Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发生一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发生其他TCP报文段。

20.iperf是一个测量网络状况的工具,-s选项表示将其作为服务器运行。iperf默认监听5001端口,并丢弃该端口上接收到的所有数据,相当于一个discard服务器。

21.service是一个脚本程序(/usr/sbin/service),它为/etc/init.d目录下的众多服务器程序(比如,httpd、vsftpd、sshd、mysqld等)的启动(start)、停止(stop)和重启(restart)等动作提供了统一的管理。

22.UDP 通信

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    - sockfd : 通信的fd
    - buf : 要发送的数据
    - len : 发送数据的长度
    - flags : 0
    - dest_addr : 通信的另外一端的地址信息
    - addrlen : 地址的内存大小

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    - sockfd : 通信的fd
    - buf : 接收数据的数组
    - len : 数组的大小
    - flags : 0
    - src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
    - addrlen : 地址的内存大小

值得一提的是,recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。

23.通用数据读写函数

socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

struct msggdr
{
    void *msg_name;             // socket地址
    socklen_t msg_namelen;      // socket地址的长度
    struct iovec* msg_iov;      // 分散的内存块
    int msg_iovlen;             // 分散内存块的数量
    void* msg_control;          // 指向辅助数据的起始位置
    socklen_t msg_controllen;   // 辅助数据的大小
    int msg_flags;              // 赋值函数中的flags参数,并在调用过程中更新
}

// msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。
// 对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读;对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写。

struct iovec
{
    void *iov_base;     // 内存起始地址
    size_t iov_len;     // 这块内存的长度
}

24.带外标记

Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接受。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:

#include <sys/socket.h>
int sockatmark(int sockfd)
    - sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,此时我们就可以利用MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
int ret = recv(connfd, buffer, BUF_SIZE-1, MSG_OOB);

25、地址信息函数

#include <sys/socket.h>
// 获取sockfd对应的本端socket地址
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);

// 获取sockfd对应的远端socket地址
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

26.广播

向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
- 只能在局域网中使用。
- 客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。

// 下面两个系统调用是专门用来读取和设置socket文件描述符属性的方法。
#include <sys/socket.h>    
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int getsockopt(int sockfd, int level, int optname, void *option_value, socklen_t* restrict option_len);

// 例如,设置广播属性的函数
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
    - sockfd : 文件描述符
    - level : SOL_SOCKET
    - optname : SO_BROADCAST
    - optval : int类型的值,为1表示允许广播
    - optlen : optval的大小

值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket只能有accept调用返回,而accept从listen监听队列中接收的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态),这说明服务器已经往被接受的连接上发送出了TCP同步报文段。

对于这种情况,Linux给开发人员提供的解决方案是:对监听socket这些socket选项,那么accpet返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。

而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已经完成。

SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓存区和发送缓存区的大小。不过,当我们用setsockopt来设置TCP的接收缓存区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。

SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读或可写。默认都是1.

SO_LINGER选项用来控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。

27.组播(多播)

  • 单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。
    单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。

    • 组播既可以用于局域网,也可以用于广域网
    • 客户端需要加入多播组,才能接收到多播的数据
  • 组播地址

    • IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 ,并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类:
IP地址说明
224.0.0.0~224.0.0.255局部链接多播地址:是为路由协议和其它用途保留的地址,路由器并不转发属于此范围的IP包
224.0.1.0~224.0.1.255预留多播地址:公用组播地址,可用于Internet;使用前需要申请
224.0.2.0~238.255.255.255预留多播地址:用户可用组播地址(临时组地址),全网范围内有效
239.0.0.0~239.255.255.255本地管理组播地址,可供组织内部使用,类似于私有 IP 地址,不能用于 Internet,可限制多播范围
  • 设置组播
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
    // 服务器设置多播的信息,外出接口
    - level : IPPROTO_IP
    - optname : IP_MULTICAST_IF
    - optval : struct in_addr
    
    // 客户端加入到多播组:
    - level : IPPROTO_IP
    - optname : IP_ADD_MEMBERSHIP
    - optval : struct ip_mreq

struct ip_mreq
{
    /* IP multicast address of group. */
    struct in_addr imr_multiaddr; // 组播的IP地址

    /* Local IP address of interface. */
    struct in_addr imr_interface; // 本地的IP地址
};

typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

28.本地套接字

  • 本地套接字的作用:本地的进程间通信
    • 有关系的进程间的通信
    • 没有关系的进程间的通信

本地套接字实现流程和网络套接字类似,一般采用TCP的通信流程。

// 本地套接字通信的流程 - tcp
// 服务器端
1. 创建监听的套接字
    int lfd = socket(AF_UNIX或AF_LOCAL, SOCK_STREAM, 0);
2. 监听套接字绑定本地的套接字文件 -> server端
    struct sockaddr_un addr;
    // 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
    bind(lfd, addr, len);
3. 监听
    listen(lfd, 100);
4. 等待并接受连接请求
    struct sockaddr_un cliaddr;
    int cfd = accept(lfd, &cliaddr, len);
5. 通信
    接收数据:read/recv
    发送数据:write/send
6. 关闭连接
    close();

// 客户端的流程
1. 创建通信的套接字
    int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的IP 端口
    struct sockaddr_un addr;
    // 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
    bind(lfd, addr, len);
3. 连接服务器
    struct sockaddr_un serveraddr;
    connect(fd, &serveraddr, sizeof(serveraddr));
4. 通信
    接收数据:read/recv
    发送数据:write/send
5. 关闭连接
    close();
// 头文件: sys/un.h
#define UNIX_PATH_MAX 108
struct sockaddr_un {
    sa_family_t sun_family;         // 地址族协议 af_local
    char sun_path[UNIX_PATH_MAX];   // 套接字文件的路径, 这是一个伪文件, 大小永远=0
}

本地套接字使用的伪文件相当于就是tcp里面的端口,因此也会出现程序重启后有一段时间不能复用端口的情况,我们可以使用在程序启动后删除该文件的做法,也可以使用unlink函数(从文件系统中删除一个名字)。例如,unlink(“server.sock”);

29.查看网络相关信息的命令

  • netstat
    参数:
    -a 所有的socket
    -p 显示正在使用socket的程序的名称
    -n 直接使用IP地址,而不通过域名服务器
    -l 显示正在监听的socket
    -t 显示TCP的socket
    -u 显示UDP的socket

30.网络信息API

// gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。
// gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。
#include <netdb.h>
struct hostent* gethostbyname(const char* name); // 不可重入、非线程安全的
    - name是目标主机的主机名
    
struct hostent* gethostbyaddr(const void* addr, size_t len, int type); // 不可重入、非线程安全的
    - addr是目标主机的IP地址
    - len是addr的长度
    - type是addr所指IP地址的类型,填AF_INET或AF_INET6.

struct hostent
{
    char* h_name;       // 主机名
    char** h_aliases;   // 主机别名列表,可能有多个
    int h_addrtype;     // 地址类型(地址族)
    int h_length;       // 地址长度
    char** h_addr_list; // 按网络字节序列出的主机IP地址列表
}
// getservbyname函数根据名称获取某个服务的完整信息,getservbyport函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取/etc/services文件来获取服务的信息的。
#include <netdb.h>
struct servent* getservbyname(const char* name, const char* proto); // 不可重入、非线程安全的
    - name是指定服务的名字
    - proto参数指定服务类型,例如"tcp"、"udp"、"NULL"(所有类型)

struct servent* getservbyport(int port, const char* proto); // 不可重入、非线程安全的
    - port是指定服务对应的端口号
    
struct servent
{
    char* s_name;       // 服务名
    char** s_aliases;   // 服务的别名列表,可能有多个
    int s_port;         // 端口号
    char* s_proto;      // 服务类型,通常是tcp或者udp
}

Linux下所有其他函数的可重入版本的命名规则,都是在原函数名尾部加上_r(re-entrant)。

// getaddrinfo函数既能通过主机名获得ip地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它的可重入版本。
#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
    - hostname可以接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)。
    - service可以接收服务名,也可以接收字符串表示的十进制端口号。
    - hints是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints可以设置为NULL,表示允许getaddrinfo反馈任何可用的结果。
    - result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
    - getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象。
    - getaddrinfo将对result隐式地分配堆内存,因此getaddrinfo调用结束后,必须使用如下配对函数来释放这块内存:void freeaddrinfo(struct addrinfo* res);

sturct addrinfo
{
    int ai_flags;
    int ai_family;                  // 地址族
    int ai_socktype;                // 服务类型,SOCK_STREAM或SOCK_DGRAM
    int ai_protocol;                // 具体的网络协议,其含义和socket系统调用的第三个参数相同,通常被设置为0
    socketlen_t ai_addrlen;         // socket地址ai_addr的长度
    char* ai_canonname;             // 主机的别名
    struct sockaddr* ai_addr;       // 指向socket地址
    struct addrinfo* ai_next;       // 指向下一个sockinfo结构的对象
}
// getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它的可重入版本。
#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);

Linux下strerror函数能将数值错误码errno转换成易读的字符串形式

31.socket的基础API中有一个sockpair函数。它能够方便地创建双向管道。其定义如下:

#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);
    - domain只能使用UNIX本地域协议族AF_UNIX,因为我们仅能在本地使用这个双向管道。
    - 创建出来的这对文件描述符都是即可读又可写的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值