第五章 Linux网络编程基础API
socket是什么? (个人理解)
socket是操作系统对主机之间网络连接的抽象和封装, 是一套专门的系统调用.
应用程序通过socket控制网络连接的细节和资源,
更形象的比喻: 主机-不同部门, 应用程序-人, socket-电话座机;
- 不同主机上的应用程序通过socket进行通信 ⇒ 不同部门的人通过电话进行交流
socket 有不同类型, 这些类型对应不同的连接类型或连接状态:
- 监听socket: 监听某个端口的socket, 如处于LISTEN状态的TCP连接
- 连接socket: 包括主动发起和由监听socket产生, 如处于ESTABLISHED状态的TCP连接
5.1 socket地址API
主机字节序和网络字节序
字节序包括
大端字节序
和小端字节序
, 大端字节序是指一个整数的高位字节存储在内存的低地址(流的前端), 小端字节序与之相反.大多数现代PC采用小端字节序, 因此小端字节序又被称为
主机字节序
为了统一, 数据传输时采用大端字节序, 因此大端字节序又叫
网络字节序
htonl: host to network long
socket地址
IP地址转换
- inet_addr的输入字符串为点分十进制的IPv4地址
- inet_aton将转化结果存储到inp指针指向的结构体内
- inet_ntoa则是将网络字节序的IPv4地址转换为点分十进制表示的IP地址
5.2 创建socket
UNIX/Linux的一个哲学是: 所有东西都是文件, socket也不例外, 它是可读写, 可控制和关闭的文件描述符.
1 |
|
- domain 指定底层协议族. PF_INET = IPv4, PF_INET6 = IPv6, PF_UNIX
- type 指定服务类型. SOCK_STREAM = TCP, SOCK_UGRAM = UDP
- protocol 用于选择具体协议, 但是在前两个参数确定时, 该值通常时唯一且等于0
- 函数调用成功后返回一个socket文件描述符, 失败则为-1
5.3 命名socket
创建socket时并未给其指定地址族, 因此还需要即将一个socket和socket地址绑定, 即socket命名.
- 服务端通常需要命名socket, 这样客户端才知道如何连接它(知道他的端口); 而客户端通常不需要命名socket, 而是采用匿名方式, 即使用OS分配的socket地址.
1 | int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); |
bind 将 my_addr 绑定到 sockfd 文件描述符上, addrlen参数指出该socket地址的长度.
- 成功返回0, 失败则返回-1
5.4 监听socket
针对服务端, socket被命名后, 还不能马上接收客户连接, 需要创建一个监听队列来存放待处理的客户连接
1 | int listen(int sockfd, int backlog); |
sockfd 指定被监听的socket, backlog 表示内核监听队列的最大长度, 队列超过该长度后, 服务器将不再受理新的客户连接, 客户端收到 ECONNREFUSED 错误信息. listen 成功返回0, 失败则是-1.
自Linux内核版本2.2之后, backlog代表处于完全连接状态(ESTABLISHED)的socket上限
5.5 接收连接
1 | int accept(int sockfd, struct sockaddr *addr, socklen _t *addrlen); |
- sockfd 时执行过listen系统调用的监听socket
- addr用来获取被接受连接的远端socket地址, 该地址的长度由addrlen指出
- accept成功时返回一个新的连接socket, 该socket唯一地标识了被接受的连接, 服务器可以通过读写该socket来实现通信; accept失败时返回-1.
accept只是从监听队列中取出连接, 而不论连接处于何种状态, 更不关心任何网络状态的变化
5.6 发起连接
服务器通过listen调用被动接收连接, 而客户端则通过connect主动发起连接
1 | int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); |
5.7 关闭连接
关闭连接实际上就是关闭该连接对应的socket
1 |
|
fd是待关闭的socket, 不过close并不是直接关闭该连接, 而是将fd的引用计数减1, 只有当计数为0时, 才真正关闭连接.
- 多进程程序中, 一次fork系统调用默认将使父进程中打开的socket引用数+1, 因此必须在父进程和子进程中都对该socket执行close调用才能关闭连接
如果想要立即终止连接, 可以使用shutdown调用
1 | int shutdown(int sockfd, int howto); |
5.8 数据读写
TCP数据读写
对文件的读写操作同样适用于socket. 此外还有专门控制socket读写的系统调用
1 | ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
- buf和len参数分别指定都缓冲区的位置和大小, recv调用成功后返回实际读取到的长度, 通常小于预期, 因此需要多次调用recv
- 同理, send成功时返回实际写入的数据长度
UDP数据读写
1 | ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, |
- UDP没有命名socket, 因此在接收和发送时需要指定socket地址
- recvfrom / sendto 也可以用于面向连接的socket的数据读写, 只需要将最后两个参数都设置为NULL
通用数据读写函数
1 | ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags); |
5.9 带外标记
Linux内核检测到TCP紧急标志后, 可以通知应用程序有带外数据需要接收, 通知方式包括IO复用产生的异常事件以及SIGURG信号. 但要想知道带外数据在数据流中的具体位置, 还需要一下系统调用
1 | int sockatmark(int sockfd); |
sockatmark判断sockfd是否处于带外标记, 即下一个被读取到的数据是否是带外数据。如果是, sockatmark返回1, 此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
5.10 地址信息函数
有时, 我们想获取一个连接socket的本端socket地址, 以及远端socket地址.
1 | int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len); |
getsockname获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存中,该socket地址的长度则存储于address_len参数指向的变量中。如果实际socket地址的长度大于address所指内存区的大小,那么该socket地址将被截断。getpeername函数则对应获取远端socket地址.
5.11 socket选项
1 | int getsockopt(int sockfd, int level, int option_name, void option_value, socklen_t* restrict option_len); |
部分socket选项只能在调用listen前针对监听socket设置才有用.
5.12 网络信息API
主机信息
1 |
|
服务信息
1 | struct servent* getservbyname(const char* name, const char* proto); |
getaddrinfo函数既能通过主机名获取IP地址, 也能通过服务名获得端口号
1 | int getaddrinfo(const char* hostname, cosnt char* service, |
getnameinfo 函数能通过socket地址同时获得以字符串表示的主机名和服务名.
1 | int getnameinfo(cosnt struct sockaddr* sockaddr, socklen_t addrlen, char* host, |
- Title: 第五章 Linux网络编程基础API
- Author: Huan Lee
- Created at : 2023-08-20 07:07:55
- Updated at : 2024-02-26 04:53:15
- Link: https://www.mirthfullee.com/2023/08/20/notion-第五章 Linux网络编程基础API-8348d8ee/
- License: This work is licensed under CC BY-NC-SA 4.0.