Linux-11


  1. socket

  socket是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,它提供了应用层进程利用网络协议交换数据的机制。socket上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。

  实际上,socket就是网络环境中进程间通信的 API,通过调用这个API,自动完成自上而的数据封装和自底而上
的数据分用工作。它是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。

  在 Linux 环境下,socket是内核借用缓冲区形成的伪文件,通过文件描述符向socket中写入数据,它会自动完成封装工作,并将封装后的数据包传送给目的端的socket,目的端的socket对该数据报进行分用最后得到实际的数据报。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递

  1. 字节序

  字典序分为大端字节序(Big-Endian)和小端字节序(Little-Endian),前者的高位数据存储在内存的低地址,后的则是在内存的高地址,即大端字节序是大端先行,而小端字节序是小端先行

  对于使用不同字节序的主机间进行通信时,容易出错,因而通常在发送端将数据转换为大端字节序,然后进行传送,接收端根据自身的字节序对接收的数据进行转换。

  TCP/IP中规定的网络字节顺序为大端字节序方式编码。

#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);
  1. socket地址

  socket地址将IP和端口号封装为结构体。

  • 通用socket地址

    • sa_family是地址族类型变量,其与协议族对应,且二者的值是一样的,而地址值对于不同协议族具有不同的含义与长度。

#include <bits/socket.h>
struct sockaddr {
    sa_family_t sa_family;
    char sa_data[14];
};
typedef unsigned short int sa_family_t;
  • 专用socket地址

  由于此前的函数编程函数都是基于IPv4协议,当时采用的都是struct sockaddr结构体,为了向前兼容,则将sockaddr退化为(void *),只要传递一个地址,然后由函数自身将该地址转换为struct sockaddr类型的指针,再根据协议族决定应该将该指针转换为哪种地址类型(AF_INET: struct sockaddr_in,位于; AF_UNIX:struct sockaddr_un,位于; AF_INET6: struct sockaddr_in6,位于)。

  所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

  1. IP地址转换(字符串转整数及其字节序转换)
#include <arpa/inet.h>
// p: 点分十进制的ip字符串,n: 网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
    // 功能:将点分十进制的ip字符串转换为网络字节序的整数
    // af: 地址族,AF_INET, AF_INET6
    // src: 点分十进制的ip字符串
    // dst: 保存结果
    // 返回值:1成功,0和-1错误
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    // 功能:将网络字节序的整数转换为点分十进制IP字符串
    // af: 地址族,AF_INET, AF_INET6
    // src: 网络字节序的整数的指针
    // dst: 保存转换结果
    // size: dst数组的大小
    // 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
  1. TCP通信流程
  • 服务器端
    • 创建一个用于监听的socket
    • 将监听用socket的文件描述符和本地的IP及端口绑定
    • 设置监听,监听用socket开始工作
    • 阻塞等待客户端发起连接,此时解除阻塞并接受客户端的连接,得到一个和客户端的socket
    • 进行通信
    • 通信结束,断开连接
  • 客户端
    • 创建一个通信用socket
    • 连接服务器,需要指定连接的服务器的IP和端口
    • 进行通信
    • 通信结束,断开连接

TCP和UDP的区别:UDP为用户数据报协议,面向无连接,可以单播、多播、广播,面向数据包,不可靠;而TCP则是传输控制协议,面向连接,可靠,基于字节流,仅支持单播传输

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

int socket(int domain, int type, int protocol);
    - 功能:创建socket
    - 参数:
        - domain: 协议族, AF_UNIX(本地进程间通信), AF_INET, AF_INET6
        - type: 通信过程中使用的协议类型
            SOCK_STREAM: 流式协议,TCP属于该类
            SOCK_DGRAM:报式协议,UDP属于该类
        - protocol: 具体的协议,一般写0,SOCK_STREAM默认使用TCP,SOCK_DGRAM默认使用UDP
    - 返回值:返回文件描述符,失败则返回-1

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
    - 功能:将socket的文件描述符和本地IP+端口进行绑定
    - 参数:
        - sockfd: 通过socket()获得的文件描述符
        - addr: 需要绑定的地址,封装了IP和端口号
        - addlen: 第二个参数的内存大小
    - 返回:0成功,-1失败

int listen(int sockfd, int backlog);
    - 功能:监听socket的连接
    - 参数:
        - sockfd:通过socket()获得的文件描述符
        - backlog: 未连接和已连接的最大值
    - 返回值:0成功,-1失败

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    - 功能:等待并接受客户端的连接,由服务器端调用,默认是阻塞的
    - 参数:
        - sockfd: 通过socket()获得的文件描述符
        - addr:  传出参数,记录了连接成功后客户端的地址信息(ip,port
        - addrlen : 指定第二个参数的对应的内存大小
    - 返回值:
        成功返回用于通信的文件描述符,失败则返回-1

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    - 功能:客户端连接服务器
    - 参数:
        - sockfd: 通信用文件描述符
        - addr: 客户端要连接的服务器的地址信息
        - addrlen: 第二个参数的内存大小

ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
  1. TCP通信协议

  TCP 是一种面向连接单播协议,在发送数据前,通信双方必须在彼此间建立一条连接,即在客户端和服务器的内存里保存关于对方的信息,如 IP 地址、端口号等

  TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接, 保证双方互相之间建立了连接,其发生于客户端连接到服务器的时候,当客户端调用了connect时,底层会通过TCP协议进行三次握手;采用四次挥手来关闭一个连接,当双方中一方调用了close(三次握手时是客户端调用connect),底层会通过TCP协议进行四次挥手。

  • 三次握手

  客户端想要发起连接,向服务器发送报文,其中标志位SYN置为1表示请求连接;服务端接收到客户端的报文后,发送报文给客户端,其中标志位ACK置为1表示确认连接,同时将SYN置为1再次表明请求连接;客户端接收到报文后,发送报文,其中ACK置1表明确认连接。至此,三次握手实现了双端的发收功能的检验并建立了连接。第一次握手,服务器可以确认客户端的发功能以及自身的收功能;第二次握手时,客户端可以确认服务器的收发功能以及自身的收发功能;第三次握手时服务器可以确认自身的发功能。如果只有两次握手,那么双方并不能确认自身以及对方的收发功能是否完备。

第一次握手:
    1. 客户端将SYN置1,向服务器请求连接
    2. 生成一个随机的32位序号seq = J
    此时服务端可以确认客户端的发功能以及自身的收功能,客户端状态为SYN_SENT,服务器状态为SYN_RCVD。
第二次握手:
    1. 服务器端接收到客户端的连接请求,将ACK置1
    2. 服务器回发一个确认序号ack,其中ack=客户端序号(J)+数据长度+SYN/FIN(按一个字节算)      // 当发送数据或者有SYN/FIN信号时ack才会变化
    3. 服务器将SYN置1,向客户端发起连接请求
    4. 生成一个随机的32位序号seq = K
    此时客户端可以确认自身的收发功能以及服务端的收发功能,客户端状态为ESTABLISHED。
第三次握手:
    1. 客户端应答服务器的连接请求,将ACK置1
    2. 客户端回发一个确认信号ack,其中ack=服务器序号(K) + 数据长度 + SYN/FIN(按一个字节算)  // 当发送数据或者有SYN/FIN信号时ack才会变化
    此时服务器可以确认客户端的收功能以及自身的发功能, 服务器状态为ESTABLISHED。
  • 滑动窗口

  滑动窗口(Sliding window)是一种流量控制技术,用于防止由于网络拥塞导致掉包的情况。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构

滑动窗口的窗长是动态变化的,可以理解为缓冲区。

  • 四次挥手
- 第一次挥手:
    1. 任一一方将FIN置1,向另一方请求断开连接
    2. 断开连接发起方发送确认序号ack=M
- 第二次挥手:
    1. 另一方接收到断开连接请求,将ACK置1
    2. 发送确认信号ack,应答断开连接请求,其中ack=断开发起方端序号(M)+数据长度+SYN/FIN(按一个字节算)
- 第三次挥手:
    1. 另一方发送FIN请求断开连接,将FIN置1
    2. 发送确认信号ack=N
- 第四挥手:
    1. 断开连接发起方接收到另一方的断开连接请求,进行应答,将ACK置1
    2. 发送确认信号ack,其中ack=另一方端序号(N)+数据长度+SYN/FIN(按一个字节算)
  • 状态转变
- 初始时客户端状态为CLOSED,而服务器为LISTEN.
- 客户端请求连接(第一次握手),客户端主动打开并发送SYN,状态转为SYN_SENT.
- 服务器应答请求,发送ACK及SYN(第二次握手),此时服务器状态切换为SYN_RCVD;而客户端接收到ACK信号后切换为ESTABLISHED.
- 客户端发送ACK应答服务器的SYN信号(第三次握手),服务器接收到信号后切换为ESTABLISHED
- 通信过程中双方状态不发生改变。
- 某一方主动发出FIN终止连接(第一次挥手),此时主动方切换为FIN_WAIT1,而被动方接收到FIN后切换为CLOSE_WAIT.
- 被动方发出ACK信号应答(第二次挥手),主动方接收信号后切换为FIN_WAIT2.
- 被动方发出FIN信号请求终止连接(第三次挥手),主动方切换为TIME_WAIT,被动方切换为LAST_ACK
- 主动方发出ACK信号应答(第四次挥手),主动方切换至CLOSED,被动方接收到ACK后切换为CLOSED.

注意,主动方切换到TIME_WAIT后并不会立即切换CLOSED,而是会定时持续两倍报文段寿命(Maximum Segment Lifetime, MSL)后方才关闭,这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。被动方会一直重传FIN直至收到最终的ACK。通常Ubuntu系统下的msl为30s
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
半关闭状态可以用于实现单方向传输的通信,其对应的API为int shutdown(int sockfd, int how);,其中sockfd为所需关闭的socket描述符,而how则是操作方式(SHUT_RD(0)关闭读功能,此时sockfd无法接收数据;SHUT_WR(1)则是关闭写功能,相当于无法发送数据;SHUT_RDWR(2)相当于close,先调用SHUT_RD,再调用一次SHUT_WR).

  1. 端口复用

  由于主动断开方最后会持续1分钟(2msl)处于TIME_WAIT状态,如果此时服务器为主动方,由于处于该状态下端口没有得到释放,此时再使用该端口会出错,因而需要利用端口复用技术来解决该问题。其主要的用途是:

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    参数: 
        - sockfd:待操作的socket文件描述符
        - level: 级别,复用端口采用SOL_SOCKET
        - optname: 选项名
            - SO_REUSERADDR
            - SO_REUSEPORT
        - optvak:端口复用的值(整型),1可以复用,0不可复用
        - optlen: optval的大小
    注意:需要在绑定之前进行设置
  1. 其他
  • netstat:查看网络相关信息的命令
    • 参数:-a查看所有socket, -p显示正在使用socket的程序名称,-直接使用IP地址而不通过域名服务器

文章作者: Vyron Su
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Vyron Su !