Linux服务器项目-8
socket通信基础
网络编程实际上是实现不同主机间进程间通信。
所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,(类似于栈先进后出)是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口(port)结合的,提供向应用层进程传送数据包的机制。
socket 本身有“插座”的意思,在 Linux 环境下,用于表h示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与有名管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
1 | // 套接字通信分两部分: |
字节序
简介
现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
==字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数
据当然就无需谈顺序的问题了)。==
字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
判断字节序:
1 | /* |
字节序转换函数:
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
1 | h - host 主机,主机字节序 |
1 |
|
应用实例:
1 | /* |
3. Socket地址
1 | // socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个 |
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
1 |
|
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:
宏 PF_ 和 AF_ 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
1 |
|
专用socket地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
用的时候创建结构体,然后为结构体内的成员赋值就行了。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
1 |
|
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和IPv6:
1 |
|
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
4. IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:
1 |
|
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
1 |
|
socket中IP地址转换函数的使用
1 | /* |
TCP通信流程
1 | // TCP 和 UDP -> 传输层的协议 |
1 | // TCP 通信的流程 |
1 | // 客户端 |
6. 套接字函数
1 |
|
利用socket实现客户端和服务端之间通信
1 | // TCP 通信的服务器端 |
一些可行的处理
1 | //char * data = "hello,i am server"; |
7. TCP 三次握手
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。
TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手来关闭一个连接。
三次握手的目的是保证双方互相之间建立了连接。三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
ACK用来做确认,SYN用来做连接,FIN用来断开连接。
- 16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。
- 32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从B 到 A)的 TCP 报文段的序号值也具有相同的含义。
- 32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示15,所以 TCP 头部最长是60 字节。
6 位标志位包含如下几项:- URG 标志,表示紧急指针(urgent pointer)是否有效。
- ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
- PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
- RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
- SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
- FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
- 16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
- 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。
- 16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
第三步的目的:确定Client端的接收没问题;ACK代表标志位,ack表示确认序号,seq代表序号,标志位为1确认需要才有效。
1 | 第一次握手: |
8. TCP 滑动窗口
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。
窗口理解为缓冲区的大小;滑动窗口的大小会随着发送数据和接收数据而变化。通信的双方都有发送缓冲区和接收数据的缓冲区。
1 | 发送方的缓冲区: |
1 |
|
1 | 四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。 |
10. TCP 通信并发
1 | 要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。 |
多进程实现TCP并发通信:
1 | //服务器端 |
多线程实现TCP通信并发实现
1 | //服务器端,多线程实现 |
11. TCP状态转换
红色实线:客户端;绿色虚线:服务端
2MSL(Maximum Segment Lifetime)最大报文生存时间
主动断开连接的一方, 最后进入一个 TIME_WAIT状态, 这个状态会持续: 2msl- msl: 官方建议: 2分钟, 实际是30s
当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
半关闭
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
从程序的角度,可以使用 API 来控制实现半连接状态:
从程序的角度,可以使用 API 来控制实现半连接状态:
1 |
|
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用
计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方
向的连接,只中止读或只中止写。
注意:
- 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
进程都调用了 close,套接字将被释放。 - 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。
但如果一个进程 close(sfd) 将不会影响到其它进程。
12. 端口复用
端口复用最常用的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
1 | 常看网络相关信息的命令netstat |
出现的问题:服务器重启有一个2MSL的时间,这段时间不能再用该端口,否则会报错bind();为了解决这个问题,可以通过端口复用。
1 |
|