引言
不知道大家有没有这样的经历,上网搜索技术文章,总是会看到
网络编程
这个字眼,而各个互联网大厂,也对掌握了网络编程
的人才,求贤若渴。其实网络编程
无处不在,我们平时用到的互联网产品和网络编程
技术息息相关。掌握网络编程
,才能在繁杂的网络世界中,看透问题本质,遇到网络相关技术问题,也才能解决的游刃有余。
目录
网络协议栈
那什么是网络编程呢?有人说http
就是网络编程,有人说开发RPC
框架是网络编程,有人说嵌入式
硬件相互通信是网络编程,其实这些都涉及网络编程,都脱离不了网络协议栈。
在大学课本《计算机网络》中,我们学过网络协议栈是OSI七层。现实中,大多数操作系统,实现的是TCP/IP
四层协议栈,如图所示。
而网络编程就是在操作系统封装的TCP/IP
协议栈的基础上,使用系统内核暴露出来的socket
网络编程Api
,进行应用程序开发。
echo
回显服务器代码
# 编译echo回显服务器
go build -o echoServer echoServer.go
# 启动服务器
./echoServer
使用
nc
,伪装echoClient
客户端
# 和服务器建立连接
nc 127.0.0.1 8888
# 客户端
hello-echo
# 服务端
hello-echo
这时候,我们通过nc
给服务器发送字符串,服务器会原样把字符串回传给我们。nc
和echoServer
整个交互过程是怎样的呢?下图展示详细过程。
网络交互图
过程详解
众所周知,TCP会有3次握手
和4次挥手
,我们的echo
服务器就是基于TCP
协议的。当然少不了3次握手
和4次挥手
。
服务器 | 建立socket内核数据结构
- 首先,我们需要创建
socket
内核数据结构,通过socket()
系统调用,我们可以告诉内核我们要建立基于ipv4
的tcp
socket套接字,内核会维护一个sock
数据结构并和一个文件相绑定,同时给我们返回一个socketfd
,供后续函数使用。 - 之后,我们需要通过
bind()
,告知内核将哪个地址绑定到该socket内核数据结构。 - 然后,使用
listen()
系统调用,将socket
转换为已监听套接字
。此时,服务器就可以进行被动连接了。
客户端 | 发起主动连接;服务器 | 被动连接
- 如果此时客户端通过
connect()
系统调用发起主动连接,客户端内核协议栈,向服务器发送三次握手的第一步SYN
。 - 当服务器收到这个
SYN
,会把该套接字放入半连接队列,并向客户端发送ACK、SYN
。 - 当客户端接受到这个
ACK、SYN
,并向服务器发送ACK
。此时客户端connect()
系统调用返回,客户端认为三次握手完成。 - 当服务器收到客户端传来的
ACK
,则将内核中的套接字放入全连接队列,等待服务器调用accept
,并返回给服务器。
服务器 | 等待已连接套接字
- 当服务器调用
accept()
, 如果此时全连接队列中没有已完成三次握手的socket,则默认会阻塞,直到全连接队列中拥有已经完成三次握手的socket。 accept()
会为之前的监听套接字sock
内核数据结构,构建一个新的文件,并分配新的fd
,这个文件称为已连接套接字。- 此时,服务器和客户端就可以收发数据了。
服务器 | 客户端 | 收发数据
收发数据为何需要调用read
和write
呢?
- 在内核中,看到的
acceptfd
(已连接套接字),本质和文件一样,acceptfd就是文件描述符,所以我们可以直接使用read
,write
这种操作文件的系统调用。 linux
内核为每个已连接套接字,分配一个接受缓冲区和一个发送缓冲区。- 对于read(),是本机读取
acceptfd
(服务器)或者socketfd
(客户端)相关的socket接收缓冲区
,如果socket接收缓冲区
缓冲区中有数据,**read()返回, 否则read()**会阻塞,直到socket接收缓冲区
中拥有了对端数据。这个接受数据的过程是由内核TCP/IP
协议栈实现的。 - 对于write(),写入的是socket发送缓冲区,如果此时发送缓冲区是满的,write()则会被阻塞。写入socket发送缓冲区的数据,由内核
TCP/IP
协议栈真实的发往对端。
这里还有几个点:
- read返回大于0,表示读取成功。
- read返回等于0,表示对端关闭连接,此时应该调用
Close
关闭连接。 - read返回小于0,表示产生错误。
- write返回小于0,页表示产生错误。
客户端 | 服务器 | 关闭连接
- 在
nc
所在的终端上键入Ctrl+c
,结束掉nc
进程。此时内核协议栈,会给服务器发送FIN
Tcp节。告知本端已经关闭。 - 服务端收到客户端的FIN,内核协议栈会给客户端回复
ACK
,同时read()
调用会返回0,这样服务器就知道客户端关闭连接了。 - 这时候,服务器应该调用
close()
,开启四次挥手的第二个阶段。向客服端发送一个FIN
。 - 当客户端收到该FIN后,内核协议栈回复ACK,自身并进入
TIME_WAIT
状态。Linux
下等待2MSL, 也就是60秒
。
至此,整个echoServer
和nc
的交互过程就讲解完了。整个过程都是正常的网络交互过程,很多异常的边界没有讨论。在这里,主要是先带大家打开网络编程
的神秘大门,有个初步映像,之后的文章我们会一步步对各种边界异常情况进行深入讲解。
网络答疑
- 假如一方断网了,不能进行4次挥手,服务器会怎么处理?
- 默认未断开一端,会保持
established
状态。 - 未断开一端开启了
Keepalive
,则会定期往对端发送TCP
保活Segment
,对端协议栈回复RST
,未断开端,就会知道已经出现异常,关闭本端连接 - 如果未断开一端没有开启
Kepalive
,则一直不知道对端已经关闭,直到往对端写数据会得到Connection reset by peer
错误,进而知道对端已经关闭。这种情况下,如果未断开端再次往断开对端写数据,则会产生EPIPE错误。 - 所以通常需要服务器在应用层做保活心跳,对这种情况的连接做定时踢掉处理。
- 客户端发送FIN之后,会发生什么?
- TCP是全双工通信,也就是双通道通信4次握手中,主动关闭端(客户端),发送FIN, 是告诉被动关闭端(服务器)我不会再给你发送数据了,你不要再在acceptFd上读取数据了,此时服务端再读取数据,会返回0, 也就是
EOF
。 - 如果服务端不需要发送数据给客户端,通常需要调用close, 服务端向客户端发送FIN,也就是4次握手的第3步。
- 如果服务端有数据要发送给客户端,此时依旧可以通过socketfd发送数据给客户端,这也是通常说的TCP半关闭状态。
后记
通过之前的讲解,我们其实可以发现:TCP/IP协议和socketApi是紧密相关的。然而它们又有很多语义的差别。很多时候,想当然的理解,并不是你所想的那样。比如close()
系统调用,会不会给对端发送FIN
,要看该已连接套接字的引用计数是否达到0。如果有多个进程共享这个文件,其中一个进程close()
,并不会给对端发送FIN
Tcp节。诸如此类的细节还有很多很多。
网络编程
很重要,学好网络编程
却不容易,如何才能掌握呢?蛇叔希望通过代码+图片的方式,给读者讲解看得见的网络编程。我们下期再见~~
参考文献
- 《TCP/IP详解 卷1》
- 《Unix网络编程 卷1》
- 《计算机网络》
首发文章,希望大家喜欢,欢迎关注
,点赞
,转发
一键三连。毕竟蛇叔掌握核心科技嘛,做不了火影主角,做个掌握核心科技的“蛇叔”也不错🤣。