CASPP Chapter 11 Network Programming 学习笔记(不定期更新)
学习进度
- Socket 套接字: 11.3.3,11.4
- Web Servers: 11.5
Socket套接字 (11.3.3, 11.4)
学习契机
学习Java内存马之前,想再看看Tomcat服务器的总体框架,其中有提到socket的内容,之前学nc的时候,也提到了socket的方法,感觉不是特别熟悉,看来还得是CSAPP!
什么是socket套接字?
什么是socket套接字?我们经常能在网络编程,客户端与服务器的通信这样的话题中聊到socket套接字,以及其相关的方法,那么存在即有意义,我们就一起来看看什么是scoket套接字以及为什么需要socket套接字。
我们都熟悉OSI七层模型或者TCP/IP模型,知道在双方通信的过程中,数据在应用层或者高层发出之后,需要经过层层打包封装后再通过物理层发送给对方,之后再层层解析拆包,最终使接收方获取到真实的数据。以上场景是非常宏观的以及抽象的描述,简单来说,socket套接字就是其中一个环节的具体实现!
我们知道在Internet通信的双方是可以建立连接的,这样的连接是 point-to-point
点对点的,而这个点 endpoint
就是 socket
,所以我们现在可以说socket就是用来帮助通信的双方建立连接,接收,以及发送数据的。
那我们刚才将socket是通信的具体实现又是连接的端点,那么每一个socket是首先是由 socket address
即套接字地址唯一标识的(待确认),其中:
socket address又两部分组成(address:port):
- IP address
- Port
那么一个连接,就是由一对 socket address
组成:(cliaddr:cliport, servaddr:servport) (客户端地址:客户端端口,服务端地址:服务端端口)
其中客户端的端口是由 kernel
自动分配的,我们叫做 ephemeral port
(临时端口?);同时服务端的端口就是预留端口用来表明特定的服务,比如Web服务器使用80端口,邮箱服务器使用25端口,相信大家应该对此都已经非常熟悉了。
socket套接字具体的功能
在上一小节中我们提到了socket可以帮助建立连接以及接收和发送数据,那么具体是如何实现的呢?
说到实现,我们就可以想到Java中的实现接口,在这里也是一样,socket套接字其实就是一个Interface 接口,那么在不同的编程语言之间都用不同的实现以及API,我们在这里就从socket interface来介绍一下socket的功能
观察上图我们可以发现,通信的双方被定义为 Client
客户端以及 Server
服务端,同时
- open_clientfd被定义为:getaddrinfo -> socket -> connect
- open_listenfd被定义为:getaddrinfo -> socket -> bind -listen
在Section 10.2的学习中,我们知道了 socket
也是Linux中的一种文件类型,我们也知道了打开文件意味着分配到一个 descriptor
描述符,由此我们可以理解为,打开一个可以用于通信的 socket 文件需要满足以上步骤,接下来我们就来具体看看这些方法
- getaddrinfo(… …)
- 用于帮助转换hostnames, host addresses, service names, and port numbers等参数的结构,以供后面的方法使用,这里暂时不展开了
- socket(… …)
- 无论是客户端还是服务端,都需要先用 socket 方法来完成打开一个文件(详见10.1的opening files操作),同时返回一个
socket decriptor
socket描述符来供应用使用
-
客户端socket
完成一个客户端socket需要执行
connect(int sockfd, const struct scokaddr *addr, ...)
方法,该方法会尝试与服务端socket的地址建立连接,并且该方法会block阻塞直到连接成功或者发生错误。
-
服务端socket
剩下的所有 socket 方法都是给服务端socket用的,包括 bind, listen, accept
bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) : -
bind()
方法会将我们之前用socket()方法创建的 socket descriptor 与 socket address地址关联起来 - 有一个问题就是为什么client socket不需要呢? - 因为不使用 bind() 方法socket address的(IP address : port number)就会由kernel自动分配,也就是我们常见的客户端了 - 而使用了 bind() 方法就意味着我们可以指定其 socket address,也就是我们所说的服务端了我们说Client socket是active主动的,是主动发起来连接的(connect());而Server socket则是被动的,是被动地等待有client等待连接的。
Kernel默认将由
socket()
方法创建的socket descriptor当作主动的socket(client),直到listen方法的出现,则会帮助kernel认出server socketlisten(int sockfd, int backlog) : - listen()方法将有一个active的socket转换为passive的socket(sockfd)来监听来自客户端的连接,而backlog则代表可以连接的数量(这里我猜测nc中如果开启了-k持续连接可能与这个参数也有关系?) - 也就是将原本的
socket()
方法返回的 sockfd 转化为listening socket
, 以此来等待来自client socket的连接int accept(int listenfd, struct sockaddr *addr, int *addrlen) : - 当server socket来自client socket发起的连接(connect())时,就会触发accept()方法 -
accept()
方法将会接收之前listen()
方法返回的listenfd,并且绑定发起连接的client socket的地址,最后返回connected descriptor
来与client socket之间使用I/O操作进行通信
-
各个file descriptors之间的关系
上面的各种方法,都会使用以及返回各种file descriptor,是不是已经有点绕了,最后我们就一起来通过一个例子辨析一下吧
- 通过socket方法创建Server socket,得到sockfd,在完成了listen()方法之后,返回得到listenfd,之后进入阻塞,等待客户端连接
- 通过socket方法创建Client socket,得到clientfd,使用connect()方法发起连接请求
- Server socket收到连接,调用 accept 方法,返回 connfd
- 最终建立起 clientfd 与 connfd 两个文件之间的连接以及通信,所有的数据的传递都通过双方对于clientfd以及connfd两个文件的读写来完成
接着我们再来辨析一下由listen()方法所返回的 listening descriptor
listenfd
与 accept()方法返回得到的connected descriptorconnfd
之间的关系:- listening descriptor是由server建立,为了client的connect()方法所服务的,一旦创建就会一直存在
- connected descriptor是由server建立了,是为了直接与client的clientfd之间通过I/O操作进行通信的,每当server接受一个连接就会创建直到服务结束
最后我们以一个问题结束这一小节的学习,为什么要区分listening以及connected descriptors? 为什么不直接用listenfd来监听连接,同时建立连接一步到位呢?
乍一看connected descriptor多次一举,但是这个分离的设计却使一个多线程并行的服务器成为可能,server只需要使用一个listenfd来处理所有client发起的连接请求,再独立的开启 connected descriptor来进行通信即可。
TODO Web Servers Web服务器
Reference
Randal E. Bryant, David R. O’Hallaron - Computer Systems. A Programmer’s Perspective [3rd ed.] (2016, Pearson)