Fork me on GitHub

Golang之旅24-socket编程

TCP/IP协议

通常使用的网络是在TCP/IP协议的基础上进行运作的。HTTP协议只是属于TCP/IP协议内部的一个子集。

TCP/IP协议是IP协议的通信过程中,使用到的协议族的总称。

TCP/IP协议族按层次分别表示为:

  • 应用层
  • 传输层
  • 网络层
  • 链路层

越往上越靠近用户,越往下越接近底层


链路层(网络接口层)

  • 物理层:处理网络连接的硬件部分,包含:控制操作系统、设备驱动、网卡和光纤等,把电脑连接起来的物理手段
  • 数据链路层: 它在物理层的上方,确定了物理层传输的0和1的分组方式及代表的意义。以太网协议统一了电信号分组方式。
  • 广播机制: 向本网络内所有计算机都发送,让每台计算机读取这个包的标头,找到接收方的MAC地址,然后与自身的MAC地址相比较;如果两者相同,就接受这个包,做进一步处理,否则丢弃。这种发送方式就叫做广播broadcasting

网络层

主要是用来处理网络上流动的数据包。该层规定了通过怎么样的传输路径到达对方,选择合适的传输路径。主要是包含IP协议用来处理各种数据包给对方。保证传输准确性的是IP协议和Mac地址。

链路层中的广播机制有一定的限制:必须保证双方在同一个子网络中,否则只能通过路由的方式发送数据。

网络层的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。 那么,每台计算机出现了两个地址:Mac地址和网络地址。

  • Mac地址是绑定在网卡上:将数据包发送到子网络的目标网卡
  • 网络地址在网络管理员分配的:确定计算机所在的子网络

传输层

提供处于网络连接中的两台计算机之间的数据传输。主要是有两个重要协议:传输控制协议TCP和用户数据报协议UDP

产生

有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?

我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

  • 0~65535,16个二进制组成
  • 系统端口:0-1023;用户使用的端口1024开始
UDP协议

我们必须在数据包中加入端口信息,于是产生了UDP协议:在数据前面加上端口号。包含标头数据两个部分,总长度不超多65535个字节。UDP协议比较简单,实现容易,但是可靠性差,一旦数据发出,无法知道对方是否收到。

  • 标头:发出端口号和接收端口号,8个字节
  • 数据:具体的数据内容
TCP协议

为了克服UDP协议的缺点,TCP协议诞生了。 TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。

TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

应用层

应用层接收到传输层传来的数据,接下来需要对数据进行解包。 应用层的作用就是规定应用程序使用的数据格式,例如TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。 它决定了向用户提供应用服务时的通信活动,比如:FTP(文件传输协议)、DNS(域名系统)服务,HTTP也处于该层

术语

  • **帧Frame:**一组电信号构成一个数据包,每个帧包含标头head和数据data两个部分,长度在64~1518字节之间;如果数据太多,可以多帧发送。

    • 标头head:包含数据的说明项,例如发送者、接收者、数据类型等,固定为18个字节
    • 数据data:数据包的具体内容,长度在46-1500字节之间
  • MAC地址:连接网络的所有接口都必须有网卡接口。数据包从一块网卡传送到另一块网卡。MAC地址(网卡的地址):数据包的发送地址和接收地址。每块网卡具有唯一的MAC地址

    • 长度是48位二进制位
    • 通常是12个十六进制数表示,6位厂商编号+6位网卡流水号

    由于实际通信中,双方在同一个LAN的情况很少,经过多台计算机和网络设备进行中转才能到达对方。

    采用ARP(address resolution protocol)协议来解析IP地址,根据通信方的IP地址就能反查出对方的MAC地址。

  • IP地址:规定网络地址的协议叫做IP协议,IP协议所定义的地址叫做IP地址。现在使用的是IPV4:32位二进制组成,范围是0.0.0.0~255.255.255.255

    根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。

    IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。

socket

Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序

socket图解

Socket是应用层与TCP/IP(Transmission Control Protocol/Internet Protocol) 协议族通信的中间软件抽象层。在设计模式中,Socket把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。

socket编程

socket是基于C/S架构的,也就是说进行socket网络编程,通常需要编写两个go文件,一个服务端,一个客户端。

TCP服务端

TCP协议是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。 服务端处理流程:

  • 监听端口
  • 接收客户端请求建立连接
  • 创建goroutine处理连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"bufio"
"fmt"
"net"
)

// TCP server端

// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Printf("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}

func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000") // 指定协议和ip地址及端口
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}

TCP客户端

一个TCP客户端进行TCP通信的流程如下:

  • 建立与服务端的连接
  • 进行数据的收发
  • 关闭连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"bufio"
"fmt"
"net"
)

// 客户端
func main() {
// 与服务端建立连接
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接

// 利用该连接进行数据的发送和接收
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
}

粘包

为什么产生粘包

TCP数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发 。粘包可以发生在发送端和接收端

  • Nagle算法造成发送端的粘包:Nagle算法是一种改善网络传输效率的算法。当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据;若有则会一次把这两段数据发送出去。
  • 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

解决办法

通过接收方对数据包进行封包和解包的操作。

  • 封包:给一段数据加上包头,数据包就包含包头和包体两个部分。
  • 包头的长度是固定的, 并且它存储了包体的长度。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

本文标题:Golang之旅24-socket编程

发布时间:2019年10月21日 - 15:10

原始链接:http://www.renpeter.cn/2019/10/21/Golang%E4%B9%8B%E6%97%8524-socket%E7%BC%96%E7%A8%8B.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Coffee or Tea