网络通信过程
- DMA:网卡和磁盘数据拷贝到内存流程比较固定,不涉及到运算操作,且非常耗时。在磁盘嵌入一个DMA芯片,完成上述拷贝工作,把CPU解脱出来,让CPU专注于运算。
- mmap:用户空间和内核空间映射同一块内存空间,从而达到省略将数据从内核缓冲区拷贝到用户空间的操作,用户空间通过映射直接操作内核缓冲区的数据。
网络I/O
- 阻塞式网络I/O
- 非阻塞式网络I/O
- 多路复用网络I/O
socket通信
- socket 把复杂的传输层协议封装成简单的接口,使应用层可以像读写文件一样进行网络数据的传输。
- socket 通信过程
网络模型
OSI参考模型
TCP/IP模型
- MSS(Maximum Segment Size, 最大分段大小):传输层数据大小的上限
- MTU(Maximum Transmit Unit, 最大传输单元):网络接口层数据大小的上限
- MSS = MTU - IP 首部-TCP 首部,MTU 视网络接口层的不同而不同。
- TCP在建立连接时通常需要协商双方的MSS 值。应用层传输的数据大于MSS 时需要分段。
TCP协议
报文格式
TCP 首部
- 前20个字节是固定的,后面还4N个可选字节(TCP选项)。
- 数据偏移:TCP数据部分距TCP开头的偏移量(一个偏移量是4个字节,4b能表示0-15),亦即TCP首部的长度。所以TCP首部的最大长度是15*4=60个字节,即TCP选项最多有40个字节。
- 端口在tcp层指定,ip在IP层指定。端口占2个字节,则最大端口号为2^16-1=65535。
- 由于应用层的数据被分段了,为了在接收端对数据按顺序重组,需要为每段数据编个“序号”。
- TCP 规定在连接建立后所有传送的报文段都必须把ACK 设置为1。
TCP 连接建立
- 第一次握手:TCP 首部SYN = 1,初始化一个序号 = J。SYN 报文段不能携带数据。
- 第二次握手:TCP 首部SYN = 1,ACK = 1,确认号 = J + 1,初始化一个序号=K。此报文同样不携带数据。
- 第三次握手:SYN = 1,ACK = 1,序号 = J + 1,确认号 = K + 1。此次一般会携带真正需要传输的数据。
- 确认号:即希望下次对方发过来的序号值。
- SYN Flood 攻击始终不进行第三次握手,属于DDOS攻击的一种。
TCP 释放连接
- TCP 的连接是全双工(可以同时发送和接收)的连接,因此在关闭连接的时候,必须关闭传送和接收两个方向上的连接。
- 第一次挥手:FIN = 1,序号 = M
- 第二次挥手:ACK = 1,序号 = M + 1
- 第三次挥手:FIN = 1,序号 = N
- 第四次挥手:ACK = 1,序号 = N + 1
- 从TIME_WAIT进入CLOSED需要经过2个MSL(Maxinum Segment Lifetime,报文最大生存时间),RFC793建议MSL = 2分钟。
TCP 编程
ResolveTCPAddr(network string, address string) (*net.TCPAddr, error)- 将主机地址解析为相应的TCP地址
- network 参数是 “tcp4” 、 “tcp6” 、 “tcp” 中的任意一个,分别表示TCP(IPv4-only),TCP(IPv6-only)或者TCP(IPv4,、IPv6的任意一个)。
- addr表示域名或者IP地址
tcpAddr, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:5656")ListenTCP(network string, laddr *net.TCPAddr) (*net.TCPListener, error)- network 选择同上
- 如果laddr 的端口字段为0,函数将选择一个当前可用的端口
listener, err := net.ListenTCP("tcp4", tcpAddr)(*net.TCPListener).Accept() (net.Conn, error):阻塞到有客户端请求连接- conn 接口:该接口代表通用的面向流的网络连接。多个线程可能会同时调用同一个Conn的方法。
type Conn interface { // Read/Write从连接中读取/写入数据,方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) // Close方法关闭该连接并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 返回远端网络地址 RemoteAddr() Addr // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞 // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作 // 参数t为零值表示不设置期限 SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error // 即使写入超时,返回值n也可能>0,说明成功写入了部分数据 SetWriteDeadline(t time.Time) error } DialTCP(network string, laddr *net.TCPAddr, raddr *net.TCPAddr) (*net.TCPConn, error)- network 选择同上
- laddr 为localAddr,可为nil。raddr 为remoteAddr
func ioutil.ReadAll(r io.Reader) ([]byte, error)- io.Reader 为conn
- 从conn中读取所有内容,直到遇到error(比如连接关闭)或EOF。
- 长连接:Server 建立连接后进入死循环接收消息,直到Client 断开连接
for { //只要client不关闭连接,server就得随时待命 n, err := conn.Read(request) //同步IO模式,如果socket上没有数据可读,该行代码会阻塞 if err == io.EOF { //对方关闭了连接 break } fmt.Printf("receive request %s\n", string(request[:n])) } - 请求和响应内容若为Struct ,需要进行序列化和反序列化 Marshal/Unmarshal
- TCP是面向字节流的,双方可以约定分隔符
UDP 协议
UDP 首部
- UDP首部占8个字节,所以UDP报文长度最小是8B。
- 不需要建立连接,直接收发数据,效率很高
- 面向报文。对应用层交下来的报文,既不合并也不拆分,直接加上边界交给IP层。TCP是面向字节流,TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送;如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。
- 从机制上不保证顺序(在IP层要对数据分段),可能会丢包(检验和如果出差错就会把这个报文丢弃掉)。在内网环境下分片乱序和数据丢包极少发生。
- 支持一对一、一对多、多对一和多对多的交互通信。
UDP 编程
- UDP 是面向报文的,一次read 刚好读好一条数据。如果由于buffer 小导致读不完一条完整的消息,则后半部分下次read 也读不到了
- 函数调用与TCP 编程类似,不做过多解释
func net.Dial(network string, address string) (net.Conn, error)
network 指定为udp,建立udp连接(伪连接,UDP无连接)。
func net.DialTimeout(network string, address string, timeout time.Duration) (net.Conn, error)
network同上
func net.ResolveUDPAddr(network string, address string) (*net.UDPAddr, error)
解析成udp地址。
func net.ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error)
直接调用Listen就返回一个udp连接。
func (*net.UDPConn).ReadFromUDP(b []byte) (int, *net.UDPAddr, error)
读数据,会返回remote的地址。
func (*net.UDPConn).WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)
写数据,需要指定remote的地址。