Socket 介绍
概述
socket是一种IPC方法,它允许位于同一主机或使用网络连接起来的不同主机的应用程序之间交换数据。
socket进行通信的方式如下:
- 各个应用程序创建一个socket,socket是一个允许通信的“设备”,两个应用程序都需要用到它。
- 服务器将自己的socket绑定到一个众所周知的地址上使得客户端能够定位到它的位置。
使用socket()系统调用能够创建一个socket,它返回一个用来在后续系统调用中引用该socket的文件描述符。
fd = socket(domain, type, protocol)
通信domain
socket存在于一个通信domain中,它确定:
- 识别出一个socket的方法(即socket“地址”的格式)
- 通信范围(是同一主机,还是网络中不同主机)
现在操作系统支持下列domain:
- UNIX(AF_UNIX)domain:允许同一主机上的应用程序之间进行通信
- IPv4(AF_INET)domain
- IPv6(AF_INET6)domain
socket类型
每个socket实现都至少提供了两种socket:流和数据报。这两种类型在UNIX和Internet domain中都得到了支持。
流socket(SOCK_STREAM)
流socket提供了一个可靠的双向的字节流通信信道:
- 可靠的
- 双向的
- 字节流
数据报socket(SOCK_DGRAM)
数据报socket允许数据以数据报的形式进行交换。在使用时无需与另一个socket简历连接。
socket 系统调用
socket()
int socket(domain, type, protocol)
- 创建一个新的socket
- type 指定socket类型,创建流socket通常指定为 SOCK_STREAM,数据报socket指定为SOCK_DGRAM
- protocol默认为0
- 返回socket的文件描述符
bind()
int bind(sockfd, struct sockaddr *addr, socklen_t addrlen)
- 将一个socket绑定到一个地址上,通常,服务器需要使用这个调用来将其socket绑定到一个地址上供客户端能够定位到该socket上
- addr参数是一个指针,指向socket绑定到的地址结构上,这个参数的结构类型取决于domain。
- 服务器可以不调用bind()直接调用listen(),这将会导致内核为该socket选择一个临时端口
- 客户端一般不需要调用,操作系统会自动绑定
通用socket地址结构:struct sockaddr
传入bind()的addr比较复杂,每种socket domain都使用了不同的地址格式,如UNIX domain socket使用路径名,而Internet domain socket 使用IP地址和端口号。struct sockaddr适用于所有domain,将各种domain特定的地址结构转换成单个类型以供socket系统调用中的各个参数使用。
listen()
- 允许一个流socket接受来自其他socket的接入连接
accept()
- 在一个监听流socket上接受来自一个对等应用程序的连接,并可选的返回对等socket地址
connect()
- 简历与另一个socket之间的连接
在大多数Linux架构上,所有这些socket系统调用实际上被实现成了通过单个系统调用socketcall()进行多路复用的库函数。
socket I/O 可以使用传统的read()和write()系统调用或使用一组socket特有的系统调用send() recv() sendto() recvfrom()。默认情况下,这些系统调用在I/O操作无法被立即完成时阻塞,使用fcntl() F_SETFL 操作用启用 O_NONBLOCK 打开文件状态标记可以执行非阻塞I/O
流socket
流程
- 每个应用程序都必须创建一个socket
- 服务端程序调用bind()将socket绑定到一个地址上,然后调用listen()通知内核它接受接入连接的意愿。
- 其他客户端程序通过connect()建立连接,同时指定需连接的socket的地址
- 服务端程序使用accept()接受连接。
- 建立连接后,进行双向数据传输直到其中一个使用close()关闭连接为止。
- 通信是通过传统的read()和write()系统调用或通过额外的一些socketAPI(send() recv())
监听接入连接:listen()
int listen(sockfd, backlog)
listen()系统调用将文件描述符sockfd引用的流socket标记为被动,这个socket后面会被用来接受来自其他(主动的)socket的链接。
无法再一个已连接的socket(已成功执行connect()的socket或由accept()调用返回的socket)上执行 listen()
如果服务器正忙于处理其他客户端,那么客户端的connect()可能并不能马上被accept(),这将产生一个未决的连接。
内核必须要记录所有未决的连接请求的相关信息,backlog参数允许限制这种未决连接的数量。在这个限制之内的连接请求会立即成功,之外的连接请求就会阻塞直到一个未决的连接被接受,并从未决连接队列中删除。
接受连接:accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
accept()系统调用会文件描述符sockfd引用的监听流socket上接受一个连入连接。如果在调用accept时不存在未决的连接,那么调用会阻塞直到有连接请求到达为止。
返回的结果是已连接的socket的文件描述符。addr参数指向一个用来返回socket地址的结构。
连接到对等socket:connect()
int connect()
流socket I/O
一对连接的流 socket 在两个端点之间提供了一个双向通信信道。
连接终止:close()
关闭一个连接之后,对等应用程序读取数据时将会收到文件结束(所有缓冲数据都读取之后),如果要写入数据,会收到一个SIGPIPE信号,并且系统调用返回EPIPE错误。
数据报 socket
流程
- 需要发送和接受数据报的应用程序都需要使用socket()创建一个数据报socket
- 服务器将socket绑定到IP地址上,客户端通过该地址发起通信。
- 要发送一个数据报,客户端调用sendto()
- 服务端调用recvfrom(),在没有数据报时会阻塞。
- 不再需要是,调用close()
无法保证顺序,也无法保证能够到达。由于底层协议有时会重新传包,也可能多次到达。
交换数据报 recvfrom() sendto()
在数据报socket上使用connect()
尽管数据报socket是无连接的,但在数据报socket上应用connect()系统调用仍然起作用,会导致内核记录这个socket的对等socket地址。
当一个数据报socket已连接后:
- 数据报的发送可在socket上使用write和send
Socket UNIX DOMAIN
UNIX domain socket地址:struct sockaddr_un
在UNIX domain中,socket地址以路径名来表示,domain特定的socket地址结构的定义如下:
为将一个UNIX domain socket绑定到一个地址上,需要初始化一个sockaddr_un结构,然后将指向这个结构的一个指针作为addr参数传入bind()并将addrlen指定为这个结构的大小。
当用来绑定UNIX domain socket时,bind()会在文件系统中创建一个条目,作为socket路径名的一部分的目录需要可访问和可写。这个文件会被标记为一个socket,当再这个路径名上应用stat()时,它会在stat结构的st_mode字段中的文件类型部分返回值S_IFSOCK。
尽管UNIX domain socket是通过路径名来标识的,但这些socket上发生的I/O无须对底层设备进行操作。
有关绑定一个UNIX domain socket的注意点:
- 无法将一个socket绑定到一个既有路径名上
- 通常将一个socket绑定到一个绝对路径上,这样这个socket就会位于文件系统中的一个固定地址处
- 一个socket只能绑定一个路径名
- 无法使用open()打开一个socket
- 当不再需要一个socket时可以使用unlink()删除其路径条目
UNIX domain中的流socket
服务器流程:
- 创建一个socket
- 删除所有与路径名一致的既有文件,这样就能将socket绑定到这个路径名上
- 为服务器socket构建一个地址结构,将socket绑定到该地址上,将这个socket标记为监听socket
- 执行一个无线循环来处理进入的客户请求,每次循环迭代执行以下任务:
- 接受一个连接,为该连接获取一个新的socket cfd
- 从已连接的socket中读取所有数据并将这些数据写入到标准输出中
- 关闭已连接的socket cfd
- 服务器手动终止(发送一个信号)
客户端流程:
- 创建一个socket
- 为服务器socket构建一个地址并连接到位于该地址处的socket
- 执行一个循环,将其标准输入复制到socket连接上,当遇到标准输入中的文件结尾时客户端就终止,其结果是客户端socket将会关闭并且服务器从连接的另一端的socket中读取数据时会看到文件结束。
UNIX domain中的数据报socket
对于UNIX domain socket来说,数据报的传输是在内核中发生的,也是可靠的,所有消息都会按序被递送并且不会发生重复的状况。
服务器创建socket后并绑定后,进入一个无线循环,在循环中使用recvfrom()接收来自客户端的数据报,将接收到的文本转换成大小格式并使用通过recvfrom()获取的地址将转换过的文本返回给客户端。
UNIX domain socket权限
socket文件的所有权和权限决定了哪些进程能够与这个socket进行通信
- 要连接一个UNIX domain流socket需要在该socket文件上拥有写权限
- 要通过一个UNIX domain数据报socket发送一个数据报需要在该socket文件上拥有写权限
- 需要在存放socket路径名的所有目录上都拥有执行权限
创建互联socket对:socketpair()
有时候让单个进程创建一对socket并将它们连接起来是比较有用的。
int socketpair(int domain, int type, int protocol, int sockfd[2])
Linux抽象socket名空间
允许将一个UNIX domain socket绑定到一个名字上但不会在文件系统中创建的名字
- 无需担心与文件系统中的既有名字产生冲突
- 没有必要在使用完socket之后删除socket路径名,当socket被关闭之后会自动删除这个抽象名
- 无需为socket创建一个文件系统路径名了,这对于chroot缓解以及在不具备文件系统上的写权限时比较有用的。
SOCKET:TCP/IP 网络基础
网络协议和层
数据链路层
要传输数据,数据链路层需要将网络层传递过来的数据报封装进被称为帧的一个一个单元。最大传输单元MTC是改层所能传输的帧大小的上限。
网络层 IP
网络层任务:
- 将数据分解成足够小的片段以便数据链路层进行传输
- 在因特网上路由数据
- 为传输层提供服务
网络层的协议是IP,IPv4使用32位地址来标识子网和主机,IPv6则使用了128位的地址。
一个裸socket(SOCK_RAW),允许程序直接与IP层进行通信,但大多数都会基于一种传输层协议之上的socket。
IP传输数据报
IP以数据报(包)的形式来传输数据。在两个主机之间发送的每一个数据报都是在网络上独立传输的,它们经过的路径可能会不同。一个IP数据报包含一个头,其大小范围为20字节到60字节。包含目标主机的地址,源地址。
一个IP实现可能会给它所支持的数据报的大小设定一个上限。所有IP实现都必须做到数据报的大小上限至少与规定的IP最小重组缓冲区大小一样大。IPv4限制值是576字节,IPv6是1500字节。
IP是无连接和不可靠的
IP是一种无连接协议,并没有在相互连接的两个主机之间提供一个虚拟电路。
IP是一种不可靠的协议:尽最大可能将数据报从发送者传输给接收者,但并不保证包到达的顺序与它们被传输的顺序一致,也不保证是否重复,甚至到达。IP也美誉错误恢复。可靠性是通过使用TCP来保证的。
IPv4为IP头提供了一个校验和,这样能够检测出头中的错误,但并没有为包中所传输的数据提供任何错误检测机制。IPv6并没有为IP头提供校验和,它依赖高层协议来完成错误检测和可靠性。
IP数据报的重复使可能发生的,数据链路层采用一些技术确保可靠性以及IP数据报可能会以隧道形式穿越采用了重传机制。
IP对数据报进行分段
IP会将数据报分段成一个个大小合适的传输单元,这些分段在到达最终目的之后会被重组成原始的数据报(每个IP分段本身就包含一个偏移量)