第五章 Linux网络编程基础API

Huan Lee Lv5

socket是什么? (个人理解)

socket是操作系统对主机之间网络连接的抽象和封装, 是一套专门的系统调用.

应用程序通过socket控制网络连接的细节和资源,

  • 更形象的比喻: 主机-不同部门, 应用程序-人, socket-电话座机;

    • 不同主机上的应用程序通过socket进行通信 ⇒ 不同部门的人通过电话进行交流

socket 有不同类型, 这些类型对应不同的连接类型或连接状态:

  • 监听socket: 监听某个端口的socket, 如处于LISTEN状态的TCP连接
  • 连接socket: 包括主动发起和由监听socket产生, 如处于ESTABLISHED状态的TCP连接

5.1 socket地址API

主机字节序和网络字节序

  • 字节序包括 大端字节序小端字节序 , 大端字节序是指一个整数的高位字节存储在内存的低地址(流的前端), 小端字节序与之相反.

    • 大多数现代PC采用小端字节序, 因此小端字节序又被称为 主机字节序

    • 为了统一, 数据传输时采用大端字节序, 因此大端字节序又叫 网络字节序

Untitled

htonl: host to network long

socket地址

Untitled

Untitled

IP地址转换

Untitled

  • inet_addr的输入字符串为点分十进制的IPv4地址
  • inet_aton将转化结果存储到inp指针指向的结构体内
  • inet_ntoa则是将网络字节序的IPv4地址转换为点分十进制表示的IP地址

5.2 创建socket

UNIX/Linux的一个哲学是: 所有东西都是文件, socket也不例外, 它是可读写, 可控制和关闭的文件描述符.

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 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
2
#include <unistd.h>
int close(int fd);
  • fd是待关闭的socket, 不过close并不是直接关闭该连接, 而是将fd的引用计数减1, 只有当计数为0时, 才真正关闭连接.

    • 多进程程序中, 一次fork系统调用默认将使父进程中打开的socket引用数+1, 因此必须在父进程和子进程中都对该socket执行close调用才能关闭连接

如果想要立即终止连接, 可以使用shutdown调用

1
int shutdown(int sockfd, int howto);

Untitled

5.8 数据读写

TCP数据读写

对文件的读写操作同样适用于socket. 此外还有专门控制socket读写的系统调用

1
2
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • buf和len参数分别指定都缓冲区的位置和大小, recv调用成功后返回实际读取到的长度, 通常小于预期, 因此需要多次调用recv
  • 同理, send成功时返回实际写入的数据长度

Untitled

UDP数据读写

1
2
3
4
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr,
socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags,
const struct sockaddr* dest_addr, socklen_t addrlen);
  • UDP没有命名socket, 因此在接收和发送时需要指定socket地址
  • recvfrom / sendto 也可以用于面向连接的socket的数据读写, 只需要将最后两个参数都设置为NULL

通用数据读写函数

1
2
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

Untitled

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
2
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(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
2
int getsockopt(int sockfd, int level, int option_name, void option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);

Untitled

部分socket选项只能在调用listen前针对监听socket设置才有用.

5.12 网络信息API

主机信息

Untitled

1
2
3
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

服务信息

Untitled

1
2
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);

getaddrinfo函数既能通过主机名获取IP地址, 也能通过服务名获得端口号

Untitled

1
2
int getaddrinfo(const char* hostname, cosnt char* service, 
const struct addrinfo* hints, struct addrinfo** result);

getnameinfo 函数能通过socket地址同时获得以字符串表示的主机名和服务名.

1
2
int getnameinfo(cosnt struct sockaddr* sockaddr, socklen_t addrlen, char* host, 
socklen_t hostlen, char* serv, socklen_t servlen, int flags);
  • 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.