套接字应用程序接口(Socket APIs)

什么是套接字


套接字是系统间一条通信链路的一个端点. 应用收发网络数据都通过一个套接字.
伯克利套接字(Berkeley sockets), 和 4.3BSD Unix 一起发布于 1983, 取得了广泛的成功.
做了些修改后, 被 POSIX 采纳.
Berkeley sockets, BSD sockets, Unix sockets, POSIX sockets, 这些词一般可以互换.
Linux, MacOS 都提供了伯克利套接字的实现.
Windows 系统的套接字被称为 Winsock.
为了使程序可以在不同系统之间移植, 编程过程中需要做一些额外的工作.

套接字可以用于 进程间通信(inter-process communication, IPC) 和 各种网络协议.

套接字的两种类型


套接字有两种基本类型, 面向连接(connect-oriented) 和 无连接(connectless).
指的是两种协议, TCP 和 UDP, TCP 是面向连接的, UDP 是无连接的.
初学者要注意, 这里的无连接并不是字面意思, 不是说真的与网络断开连接.

无连接的协议里, 如 UDP, 每个数据包都独立的寻址.
在协议看来, 每个数据包都是完全独立的, 与之前的包, 之后的包没有任何关系.
用 明信片 做一个 UDP 的类比.
寄出一张明信片, 不保证其一定能到达, 也没办法知道其是否到达.
如果一次性发出多张明信片, 也没办法预知他们到达的顺序.
最后发出的明信片已经到达了, 而在此几周以后, 最先发出的明信片才到达, 这是完全有可能的.
同样, UDP 也不保证包一定能到达, 就算包没有到达( 丢包), 也无法知道.
UDP 也不保证包按照发送的顺序依次到达.
UDP 甚至比明信片更不可靠, 包有可能到达两次.

如果要实现可靠的通信, 需要建立一套完整的方案.
首先, 要给待发送的包编号.
第一个发出的包编号为 1, 第二个发出的包编号为 2, 以此类推.
还必须要求接收者为收到的每个包都发回一个确认.
这样, 接收者可以知道收到的包其正确顺序是什么样的.
如果相同的包到达两次, 接收者可以选择忽略其中一个.
如果有包没有收到, 发送者也可以通过缺失了的确认推理出具体是那些包没有收到, 然后重发这些包.
这套方案就是面向连接的协议要做的事情, 如 TCP.
TCP 保证数据到达的顺序和发送的顺序是相同的.
数据重复到达了, 消除冗余; 数据丢失了, 重发.
TCP 还提供一些额外的功能, 如 连接终止时的通知, 减轻网络拥塞的算法.
而且, TCP 的这些功能并不是按照" 以 UDP 为基础, 定制一套方案" 的方式实现的.

HTTP, SSH, SMTP 都使用 TCP.
DNS 使用 UDP, 原因是 DNS 请求 和 DNS 回应 足够小, 一个包就可以装下.
UDP 经常用于实时(real-time) 应用, 如音频流, 视频流, 多人视频游戏.
这些应用如果出现丢包, 通常没有重传的必要.
如果发出的消息不期望收到回应, 使用 UDP 更合适, 因此, 在配合 IP 广播或多播时, UDP 就很有用.
而 TCP 需要双向通信才能提供各种保证, 也就不适于配合 IP 广播和多播.
如果不需要 TCP 提供的各种保证, 使用 UDP 更有效率一些.
TCP 要给包编号, 这增加了额外的开销.
如果包乱序到达, TCP 会延迟包的传送, 这在实时应用里会导致非必要的延迟.

套接字函数


socket()创建并初始化一个新套接字.
bind()在一个套接字和一个特定的(IP 地址, 端口) 之间建立关联.
listen()服务端, 通过一个 TCP 套接字监听新连接.
connect()客户端, 设置远端的地址和端口. TCP 情况下, 会建立一个连接.
accept()服务端, 为 入方向(incoming) TCP 连接创建一个新套接字.
send()/resv()通过套接字 发送/接收 数据.
sendto()/resvfrom()通过尚未绑定远端地址的套接字 发送/接收 数据.
close()关闭一个套接字. TCP 情况下, 会终止一个连接.
shutdown()关闭一个 TCP 连接的一侧, 确保连接的有序拆除.
select()在一个或多个套接字上等待一个事件.
getnameinfo()/getaddrinfo()提供一种协议无关(protocol-independent) 的方式使用 主机名 和 地址.
setsockopt()改变一些套接字选项.
fcntl()同样是用来 获取/设置 一些套接字选项.

考虑到程序在不同系统之间的可移植性, 有些函数( 系统调用) 应尽量避免使用, 如 read()/write().

套接字程序的工作方式


  • 客户端-服务器模型(client-server, C/S)
  • 服务器在其发布的地址上监听是否有新连接, 知道该地址的客户端发起建立连接.
    连接建立之后, 客户端和服务器就都可以收发数据, 直到其中一方终止连接.
    客户端和服务器有着不同的行为.
    比如网页浏览, 服务器驻留在某个已知的地址等待连接.
    客户端(web 浏览器) 建立连接并发送请求, 请求中说明其要下载的页面和资源.
    服务器检查请求并作出合适的回应.

  • 点对点模型(peer-to-peer, P2P)
  • 点对点模型里, 每个点基本上都具有相同的责任.
    C/S 模型, 服务器可以对其向客户端返回请求数据的过程进行优化, 而点对点协议的数据交换过程, 其点与点之间的关系更平等.
    即便如此, 点对点模型里, 创建的底层套接字(TCP 的 或 UDP 的) 也并不一样.
    即是说, 一个点对点连接, 一侧负责监听, 另一侧负责连接.

    BitTorrent 使用的就是点对点模型.
    其工作方式是, 存在一个中央服务器, 称为追踪者(tracker), 上面存有一张表单, 表单里记录着很多点的 IP 地址.
    表单上的每个点都同意像服务器一样监听新连接.
    当有新的点要加入群, 就向中央服务器请求该表单, 然后就尝试向表单上的每个点发起连接, 同时也监听来自其他点的新连接.
    总之, 点对点协议不是要取代 C/S 模型, 只是期望每个点都既是服务器又是客户端.

  • FTP
  • FTP 的工作方式和 C/S 模型也有些不同.
    FTP 服务器监听连接直到有 FTP 客户端发起连接.
    初始连接完成后, 客户端向服务器发出命令.
    如果客户端要从服务器请求一个文件, 为了传送整个文件, 服务器会尝试向客户端发起连接.
    也就是说, FTP 客户端先是像 TCP 客户端一样发起连接, 接着, 又像 TCP 服务器一样接受连接.

网络程序一般可以描述成这四种类型之一: TCP 服务器, TCP 客户端, UDP 服务器, UDP 客户端.
有些协议要求程序实现其中的两种, 甚至所有这四种.

TCP 程序流程


TCP 程序流程

TCP 客户端必须知道服务器的地址.
用户输入地址以后, 客户端通过调用 getaddrinfo() 将 TCP 服务器地址 解析成 struct addrinfo 结构.
调用 socket() 创建一个套接字, 接着调用 connect() 发起建立一个新 TCP 连接.
这时, 就可以通过调用 send()/recv() 交换数据.

TCP 服务器在一个特定网络接口和特定端口号上监听是否有新连接.
首先, 程序使用适合用于监听的 IP 地址和端口号 初始化一个 struct addrinfo 结构.
调用 socket() 创建一个套接字.
调用 bind(), 将该套接字绑定到监听 IP 地址和端口上.
调用 listen(), 将套接字置为监听状态.
调用 accept(), 等待客户端发起建立新连接.
新连接建立以后, accept() 会返回一个新套接字.
通过这个新套接字, 服务器可以调用 send()/recv() 与客户端交换数据.
同时, 之前的套接字则继续监听新连接.
重复调用 accept(), 服务器就可以处理多个客户端.

UDP 程序流程


UDP 程序流程

为了发送第一个包, 要求 UDP 客户端必须知道远端 UDP 点的地址.
UDP 客户端使用 getaddrinfo() 函数将地址解析成一个 struct addrinfo 结构.
解析完成后, 创建一个合适类型的套接字, 然后, 调用 sendto() 发送第一个包.
继而, 通过在该套接字上继续调用 sendto()/recvfrom(), 客户端就可以 发送/接收 额外的包.

UDP 服务器监听来自 UDP 客户端的连接.
通过调用 getaddrinfo() 函数, 以一种 协议无关 的方式, 用适合监听的 IP 地址和端口号初始化一个 struct addrinfo 结构.
调用 socket() 创建一个套接字.
调用 bind(), 将该套接字绑定到用于监听的 IP 地址和端口上.
这时, 服务器调用 recvfrom() 进入 阻塞(block) 状态, 直到从 UDP 客户端收到数据.
第一个数据收到以后, 服务器可以通过调用 sendto() 做出回应, 或者继续调用 recvfrom() 监听更多的数据( 有可能来自之前的客户端, 也有可能来自新客户端).

注意


对于 TCP, 连接的哪一侧先调用 send() 或 recv(), 调用多少次, 都是没有规则的.
连接建立的同时, 双方就都可以调用 send().
如果用于连接的网络接口比较特殊, TCP 客户端可以在调用 connect() 之前先调用 bind().
比如, 如果服务器上存在多个网络接口, 那么, 允许由客户端去指定连接哪个接口, 有些时候就很重要.

对于 UDP, 是客户端通过调用 sendto() 发送的第一个包.
远端 UDP 点在收到来自客户端的数据之前, 是无法知道该向哪里发送数据的, 所以, 不能是客户端先收到数据.
这一点与 TCP 不同, TCP 是通过握手先建立起连接.
连接建立之后, 客户端和服务器就都可以发出第一个应用数据.

一个简单的时间服务器(Linux)


  1. /* network.h */
  2. #ifndef NETWORK_H
  3. #define NETWORK_H 1
  4. #include <sys/types.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <netdb.h>
  9. #include <unistd.h>
  10. #include <errno.h>
  11. #endif
  1. /*
  2. time_server.c
  3. struct addrinfo {
  4. int ai_flags;
  5. int ai_family;
  6. int ai_socktype;
  7. int ai_protocol;
  8. socklen_t ai_addrlen;
  9. struct sockaddr *ai_addr;
  10. char *ai_canonname;
  11. struct addrinfo *ai_next;
  12. }
  13. 编译, 运行, 在浏览器地址栏输入 http://127.0.0.1:8080 或 http://[::1]:8080, 应该就能看到回应.
  14. */
  15. #include <stdio.h>
  16. #include <stdlib.h>
  17. #include <string.h>
  18. #include <time.h>
  19. #include "network.h"
  20. int main(int argc, char ** argv) {
  21. printf("Configuring local address ...\n");
  22. struct addrinfo info_local;
  23. memset(&info_local, 0, sizeof(info_local));
  24. /* info_local.ai_family = AF_INET; */
  25. info_local.ai_family = AF_INET6;
  26. info_local.ai_socktype = SOCK_STREAM;
  27. info_local.ai_flags = AI_PASSIVE;
  28. struct addrinfo * addr_local;
  29. getaddrinfo(NULL, "8080", &info_local, &addr_local);
  30. printf("Creating socket ...\n");
  31. int socket_listen = socket(
  32. addr_local->ai_family, addr_local->ai_socktype, addr_local->ai_protocol
  33. );
  34. if (socket_listen < 0) {
  35. fprintf(stderr, "socket() failed, errno: %d, %s\n", errno, strerror(errno));
  36. return EXIT_FAILURE;
  37. }
  38. printf("Supporting dual-stack, IPv4 and IPv6 ...\n");
  39. int option = 0;
  40. if (setsockopt(socket_listen, IPPROTO_IPV6, IPV6_V6ONLY,
  41. (void *) &option, sizeof(option))) {
  42. fprintf(stderr, "setsockopt() failed, errno: %d, %s\n", errno, strerror(errno));
  43. return EXIT_FAILURE;
  44. }
  45. printf("Binding socket to local address ...\n");
  46. if (bind(socket_listen, addr_local->ai_addr, addr_local->ai_addrlen)) {
  47. fprintf(stderr, "bind() failed, errno: %d, %s\n", errno, strerror(errno));
  48. return EXIT_FAILURE;
  49. }
  50. freeaddrinfo(addr_local);
  51. printf("Listening ...\n");
  52. if (listen(socket_listen, 16) < 0) {
  53. fprintf(stderr, "listen() failed, errno: %d, %s\n", errno, strerror(errno));
  54. return EXIT_FAILURE;
  55. }
  56. printf("Waiting for connection ...\n");
  57. struct sockaddr_storage addr_client;
  58. socklen_t client_len = sizeof(addr_client);
  59. int socket_client = accept(
  60. socket_listen, (struct sockaddr *) &addr_client, &client_len
  61. );
  62. if (socket_client < 0) {
  63. fprintf(stderr, "accept() failed, errno: %d, %s\n", errno, strerror(errno));
  64. return EXIT_FAILURE;
  65. }
  66. printf("Client is connected ...\n");
  67. char addr_buff[1024];
  68. getnameinfo(
  69. (struct sockaddr *) &addr_client, client_len,
  70. addr_buff, sizeof(addr_buff), 0, 0, NI_NUMERICHOST
  71. );
  72. printf(" --> %s\n", addr_buff);
  73. printf("Reading request ...\n");
  74. char request[1024];
  75. int bytes_recv = recv(socket_client, request, sizeof(request), 0);
  76. printf("Received %d bytes\n", bytes_recv);
  77. printf("Received: %.*s", bytes_recv, request);
  78. printf("Sending response ...\n");
  79. const char * response =
  80. "HTTP/1.1 200 OK\r\n"
  81. "Connection: close\r\n"
  82. "Content-Type: text/plain\r\n\r\n"
  83. "Local time: ";
  84. int bytes_sent = send(socket_client, response, strlen(response), 0);
  85. printf("Header: Sent %d of %d bytes\n", bytes_sent, (int) strlen(response));
  86. time_t now;
  87. time(&now);
  88. char * time_msg = ctime(&now);
  89. printf("Local time: %s\n", time_msg);
  90. bytes_sent = send(socket_client, time_msg, strlen(time_msg), 0);
  91. printf("Content: Sent %d of %d bytes\n", bytes_sent, (int) strlen(time_msg));
  92. printf("Closing connection ...\n");
  93. close(socket_client);
  94. printf("Closing listening socket ...\n");
  95. close(socket_listen);
  96. printf("Finished ...\n");
  97. return EXIT_SUCCESS;
  98. }

《C 语言网络编程实践》(《Hands-On Network programming with C》)