Lec 2.8: TCP Socket Programming
基本流程
-
服务器进程必须先处于运行状态
- 创建欢迎 socket
- 和本地端口捆绑(比如 Web 服务就和 TCP 80/443 捆绑)
- 在欢迎 socket 上阻塞式等待接受用户的连接
- 阻塞式:除非操作系统接收到了用户的连接,否则 OS 的进程调度程序在 context switch 的时候,就不会选择让这个进程继续运行
-
客户端主动和服务器建立连接
- 创建本地套接字(隐式捆绑到本地的某个高位 port,比如 51411)
- 指定(服务器进程的)IP 地址和端口号,与服务器进程连接
-
当与客户端连接请求到来时
-
服务器接受来自用户端的请求,解除阻塞式等待,返回一个新的 socket,与客户端通信
- 注意:新的 socket 不是原来的欢迎 socket
好处:
- 允许服务端与多个客户端通信
- 使用源 IP 和源端口来区分不同的客户端
-
-
连接 API 调用有效时,客户端就与服务器建立了 TCP 连接
数据结构
sockaddr_in
:IP 地址和端口的数据结构
struct sockaddr_in
{
short sin_family; // AF_INET
u_short sin_port; // port
struct in_addr sin_addr;
// IP address, uint32_t
char sin_zero[8]; // align
};
如图,sin_family
支持多种协议。
hostent
:域名和 IP 地址的数据结构
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
};
#define h_addr h_addr_list[0] /* for backward compatibility */
hostent
作为调用域名解析函数时的参数。返回后,将 IP 地址拷贝到 sockaddr_in
的 IP 地址部分。
TCP 连接
如图,注意
connectionSocket = accept(welcomeSocket)
就是一个阻塞函数,直到welcomeSocket
收到(合法)连接请求之后,才取消阻塞,并且在成功建立连接之后,得到一个新的connectionSocket
。sad
就是 socket address,对应sockaddr_in
结构体。- 图上是单进程的模式。实际上,我们可以在
connectionSocket = accept(...)
后面进行fork
,用子进程来处理connectionSocket
,而父进程还是在welcomeSocket
上监听。- 由于 TCP 连接是一个四元组(自身 IP:port,对方 IP:port),因此任何发送到 80 端口的数据包,都能和唯一一个进程的 fd 对应上。
- 因此,不必考虑多进程时的问题。
- 图中黑线是连接建立过程,红线是实际数据包发送过程。
- 图中红线显示的是“先发再收”。但是,普遍而言,“先发再收”还是“先收再发”,取决于应用协议的具体定义。
UDP 连接 VS TCP 连接
对于 TCP 而言,客户端分这几个步骤:
- 初始化 socket file descriptor
- 执行 dns 查询,查找 host 对应的的 ip address
- 通过端口、IP 构建 sockaddr_in
- 指定对方 IP 和端口
- 自己的 IP 和端口隐式绑定
- 发起连接建立请求
- send
- receive
- 关闭连接
对于 TCP 而言,服务端分这几个步骤
- 初始化 welcome socket file descriptor
- 通过端口、IP 构建 sockaddr_in
- 指定自己 IP 和端口
- IP 经常为
INADDR_ANY
,INADDR_LOOPBACK
等等
- 阻塞式 listen
- listen 时,需要绑定
struct sockaddr_in client_socket
,以便于知道对方的 IP 和端口
- listen 时,需要绑定
- 创建 connection socket descriptor
- receive
- (process query)
- send
- 关闭连接
对于 UDP 而言,客户端分这几个步骤:
- 初始化 socket file descriptor
- 执行 dns 查询,查找 host 对应的的 ip address
- 通过端口、IP 构建 sockaddr_in
- 指定对方 IP 和端口
- 自己的 IP 和端口隐式绑定
- sendto(需要指定 socketaddr_in)
- recvfrom(需要指定 socketaddr_in)
与 TCP 不同:
- 无需事先建立连接
- 需要指定地址
对于 UDP 而言,服务端分这几个步骤
- 初始化 socket file descriptor
- 通过端口、IP 构建 sockaddr_in
- 指定自己 IP 和端口
- IP 经常为
INADDR_ANY
,INADDR_LOOPBACK
等等
- recvfrom(需要绑定
socketaddr_in client_addr
,以便于知道对方的 IP 和端口) - (process query)
- sendto(需要指定
socketaddr_in client_addr
)
与 TCP 不同:
- 无需事先建立连接
- 需要指定地址
- 无需创建新的 connection socket 用于服务这个连接
Lec 3.1: Introduction
如图,TCP 和 UDP 都提供了多路复用和解复用的服务(也就是说,IP 只提供了主机到主机的服务,因此,如果主机里面的进程希望通信,那么就需要复用 IP 提供的这一条链路,使之看起来就和多路一样)。
但是,除此之外,UDP 不再提供任何其它服务,而 TCP 还提供了可靠、保序的服务,以及拥塞控制、流量控制等额外功能。为了实现这些功能,TCP 需要首先先建立连接。
当然,不论是 TCP 还是 UDP,它们能够提供的服务都是有限的。对于延时和带宽,它们无能为力。
Lec 3.2: Mux and Demux
TCP
TCP 套接字需要提供 (Source IP, Source Port, Dest IP, Dest Port)
这一个四元组。通过这个四元组,我们就可以唯一标识一个 TCP 连接。
具体地:
- 通过 socket,我们将四元组以 IDU 的形式传输给 TCP
- TCP 将
(Source Port, Dest Port)
封装到自己的 PDU 的 header 里去,然后把(Source IP, Dest IP)
继续当作 ICI,传给 IP - IP 将
(Source IP, Dest IP)
封装到自己的 PDU 的 header 里去,并一路传到对方的对等层 - 对方的 IP 将 TCP segment 以及 IP 信息传给 TCP
- 对方的 TCP 将 HTTP 数据包以及 IP, port 信息传给 HTTP,从而完成了一次发送
UDP
UDP 相比 TCP 的套接字,创建的时候,无需指定对方的 IP 和 port,但是进行传输的时候需要指定。所以,具体的过程和 TCP 其实是大同小异的。
UDP 和 TCP 定位进程方式的区别
但是,UDP 只以目标 IP 和目标 port 来区分进程。因此,如果多个不同的主机指定了同一个 IP:port,那么就会被转发到同一个对方主机的 PID 上处理。
而 TCP 还考虑了源 IP,port,因此可以不同的 connection socket 服务不同的 source IP, port。
如果要通过代码获得一些“感性上的认知”的话。如下
TCP 是:
/**
* tcp-server.c
*/
int main()
{
//...
connection_socket = accept(welcome_socket, (struct sockaddr *)&client_address, &client_address_len);
}
在这里面,kernel 就把 client_address 和 welcome_socket 绑定在一起,形成四元组,然后返回这个 file descriptor。
而 UDP 是:
/**
* udp-server.c
*/
int main()
{
// ...
ssize_t bytes_received = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_address, &addr_len);
}
在这里面,根本没有新的 file descriptor 被创建,显然只能是采用二元组。