目录html
更新、更全的《Go从入门到放弃》的更新网站,更有python、go、人工智能教学等着你:http://www.javashuo.com/article/p-mxrjjcnn-hn.htmlpython
如今咱们几乎天天都在使用互联网,咱们前面已经学习了如何编写Go语言程序,可是如何才能让咱们的程序经过网络互相通讯呢?本章咱们就一块儿来学习下Go语言中的网络编程。 关于网络编程实际上是一个很庞大的领域,本文只是简单的演示了如何使用net包进行TCP和UDP通讯。如需了解更详细的网络编程请自行检索和阅读专业资料。程序员
互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何链接和组网。咱们理解了这些协议,就理解了互联网的原理。因为这些协议太过庞大和复杂,没有办法在这里一律而全,只能介绍一下咱们平常开发中接触较多的几个协议。算法
互联网的逻辑实现被分为好几层。每一层都有本身的功能,就像建筑物同样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感受到下面的几层。要理解互联网就须要自下而上理解每一层的实现的功能。 如上图所示,互联网按照不一样的模型划分会有不用的分层,可是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中咱们使用最多的是上图中将互联网划分为五个分层的模型。编程
接下来咱们一层一层的自底向上介绍一下每一层。设计模式
咱们的电脑要与外界互联网通讯,须要先把电脑链接网络,咱们能够用双绞线、光纤、无线电波等方式。这就叫作”实物理层”,它就是把电脑链接起来的物理手段。它主要规定了网络的一些电气特性,做用是负责传送0和1的电信号。浏览器
单纯的0和1没有任何意义,因此咱们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每一个信号位有何意义?这就是”数据连接层”的功能,它在”物理层”的上方,肯定了物理层传输的0和1的分组方式及表明的意义。早期的时候,每家公司都有本身的电信号分组方式。逐渐地,一种叫作”以太网”(Ethernet)的协议,占据了主导地位。bash
以太网规定,一组电信号构成一个数据包,叫作”帧”(Frame)。每一帧分红两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,好比发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。所以,整个”帧”最短为64字节,最长为1518字节。若是数据很长,就必须分割成多个帧进行发送。网络
那么,发送者和接受者是如何标识呢?以太网规定,连入网络的全部设备都必须具备”网卡”接口。数据包必须是从一块网卡,传送到另外一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫作MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,一般用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就能够定位网卡和数据包的路径了。架构
咱们会经过ARP协议来获取接受方的MAC地址,有了MAC地址以后,如何把数据准确的发送给接收方呢?其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内全部计算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,而后与自身的MAC地址相比较,若是二者相同,就接受这个包,作进一步处理,不然就丢弃这个包。这种发送方式就叫作”广播”(broadcasting)。
按照以太网协议的规则咱们能够依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就能够找到身在世界另外一个角落的某台电脑的网卡了,可是这种作法有一个重大缺陷就是以太网采用广播方式发送数据包,全部成员人手一”包”,不只效率低,并且发送的数据只能局限在发送者所在的子网络。也就是说若是两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,由于若是互联网上每一台计算机都会收到互联网上收发的全部数据包,那是不现实的。
所以,必须找到一种方法区分哪些MAC地址属于同一个子网络,哪些不是。若是是同一个子网络,就采用广播方式发送,不然就采用”路由”方式发送。这就致使了”网络层”的诞生。它的做用是引进一套新的地址,使得咱们可以区分不一样的计算机是否属于同一个子网络。这套地址就叫作”网络地址”,简称”网址”。
“网络层”出现之后,每台计算机有了两种地址,一种是MAC地址,另外一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助咱们肯定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。所以,从逻辑上能够推断,一定是先处理网络地址,而后再处理MAC地址。
规定网络地址的协议,叫作IP协议。它所定义的地址,就被称为IP地址。目前,普遍采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,咱们一般习惯用分红四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。
根据IP协议发送的数据,就叫作IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。
有了MAC地址和IP地址,咱们已经能够在互联网上任意两台主机上创建通讯。但问题是同一台主机上会有许多程序都须要用网络收发数据,好比QQ和浏览器这两个程序都须要链接互联网并收发数据,咱们如何区分某个数据包究竟是归哪一个程序的呢?也就是说,咱们还须要一个参数,表示这个数据包到底供哪一个程序(进程)使用。这个参数就叫作”端口”(port),它实际上是每个使用网卡的程序的编号。每一个数据包都发到主机的特定端口,因此不一样的程序就能取到本身所须要的数据。
“端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。有了IP和端口咱们就能实现惟一肯定互联网上一个程序,进而实现网络间的程序通讯。
咱们必须在数据包中加入端口信息,这就须要新的协议。最简单的实现叫作UDP协议,它的格式几乎就是在数据前面,加上端口号。UDP数据包,也是由”标头”和”数据”两部分组成:”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包很是简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
UDP协议的优势是比较简单,容易实现,可是缺点是可靠性较差,一旦数据包发出,没法知道对方是否收到。为了解决这个问题,提升网络可靠性,TCP协议就诞生了。TCP协议可以确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上能够无限长,可是为了保证网络的效率,一般TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包没必要再分割。
应用程序收到”传输层”的数据,接下来就要对数据进行解包。因为互联网是开放架构,数据来源五花八门,必须事先规定好通讯的数据格式,不然接收方根本没法得到真正发送的数据内容。”应用层”的做用就是规定应用程序使用的数据格式,例如咱们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。
以下图所示,发送方的HTTP数据通过互联网的传输过程当中会依次添加各层协议的标头信息,接收方收到数据包以后再依次根据协议解包获得数据。
Socket是BSD UNIX的进程通讯机制,一般也称做”套接字”,用于描述IP地址和端口,是一个通讯链的句柄。Socket能够理解为TCP/IP网络的API,它定义了许多函数或例程,程序员能够用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序一般经过”套接字”向网络发出请求或者应答网络请求。
Socket
是应用层与TCP/IP协议族通讯的中间软件抽象层。在设计模式中,Socket
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket
后面,对用户来讲只须要调用Socket规定的相关函数,让Socket
去组织符合指定的协议数据而后进行通讯。
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向链接(链接导向)的、可靠的、基于字节流的传输层(Transport layer)通讯协议,由于是面向链接的协议,数据像水流同样传输,会存在黏包问题。
一个TCP服务端能够同时链接不少个客户端,例如世界各地的用户使用本身电脑上的浏览器访问淘宝网。由于Go语言中建立多个goroutine实现并发很是方便和高效,因此咱们能够每创建一次连接就建立一个goroutine去处理。
TCP服务端程序的处理流程:
咱们使用Go语言的net包实现的TCP服务端代码以下:
// tcp/server/main.go // 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.Println("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") 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处理链接 } }
将上面的代码保存以后编译成server
或server.exe
可执行文件。
一个TCP客户端进行TCP通讯的流程以下:
使用Go语言的net包实现的TCP客户端代码以下:
// tcp/client/main.go // 客户端 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])) } }
将上面的代码编译成client
或client.exe
可执行文件,先启动server端再启动client端,在client端输入任意内容回车以后就可以在server端看到client端发送的数据,从而实现TCP通讯。
服务端代码以下:
// socket_stick/server/main.go func process(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) var buf [1024]byte for { n, err := reader.Read(buf[:]) if err == io.EOF { break } if err != nil { fmt.Println("read from client failed, err:", err) break } recvStr := string(buf[:n]) fmt.Println("收到client发来的数据:", recvStr) } } func main() { listen, err := net.Listen("tcp", "127.0.0.1:30000") if err != nil { fmt.Println("listen failed, err:", err) return } defer listen.Close() for { conn, err := listen.Accept() if err != nil { fmt.Println("accept failed, err:", err) continue } go process(conn) } }
客户端代码以下:
// socket_stick/client/main.go func main() { conn, err := net.Dial("tcp", "127.0.0.1:30000") if err != nil { fmt.Println("dial failed, err", err) return } defer conn.Close() for i := 0; i < 20; i++ { msg := `Hello, Hello. How are you?` conn.Write([]byte(msg)) } }
将上面的代码保存后,分别编译。先启动服务端再启动客户端,能够看到服务端输出结果以下:
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
客户端分10次发送的数据,在服务端并无成功的输出10次,而是多条数据“粘”到了一块儿。
主要缘由就是tcp数据传递模式是流模式,在保持长链接的时候能够进行屡次的收和发。
“粘包”可发生在发送端也可发生在接收端:
出现”粘包”的关键在于接收方不肯定将要传输的数据包的大小,所以咱们能够对数据包进行封包和拆包的操做。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部份内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,而且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
咱们能够本身定义一个协议,好比数据包的前4个字节为包头,里面存储的是发送的数据的长度。
// socket_stick/proto/proto.go package proto import ( "bufio" "bytes" "encoding/binary" ) // Encode 将消息编码 func Encode(message string) ([]byte, error) { // 读取消息的长度,转换成int32类型(占4个字节) var length = int32(len(message)) var pkg = new(bytes.Buffer) // 写入消息头 err := binary.Write(pkg, binary.LittleEndian, length) if err != nil { return nil, err } // 写入消息实体 err = binary.Write(pkg, binary.LittleEndian, []byte(message)) if err != nil { return nil, err } return pkg.Bytes(), nil } // Decode 解码消息 func Decode(reader *bufio.Reader) (string, error) { // 读取消息的长度 lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据 lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "", err } // Buffered返回缓冲中现有的可读取的字节数。 if int32(reader.Buffered()) < length+4 { return "", err } // 读取真正的消息数据 pack := make([]byte, int(4+length)) _, err = reader.Read(pack) if err != nil { return "", err } return string(pack[4:]), nil }
接下来在服务端和客户端分别使用上面定义的proto
包的Decode
和Encode
函数处理数据。
服务端代码以下:
// socket_stick/server2/main.go func process(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) for { msg, err := proto.Decode(reader) if err == io.EOF { return } if err != nil { fmt.Println("decode msg failed, err:", err) return } fmt.Println("收到client发来的数据:", msg) } } func main() { listen, err := net.Listen("tcp", "127.0.0.1:30000") if err != nil { fmt.Println("listen failed, err:", err) return } defer listen.Close() for { conn, err := listen.Accept() if err != nil { fmt.Println("accept failed, err:", err) continue } go process(conn) } }
客户端代码以下:
// socket_stick/client2/main.go func main() { conn, err := net.Dial("tcp", "127.0.0.1:30000") if err != nil { fmt.Println("dial failed, err", err) return } defer conn.Close() for i := 0; i < 20; i++ { msg := `Hello, Hello. How are you?` data, err := proto.Encode(msg) if err != nil { fmt.Println("encode msg failed, err:", err) return } conn.Write(data) } }
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无链接的传输层协议,不须要创建链接就能直接进行数据发送和接收,属于不可靠的、没有时序的通讯,可是UDP协议的实时性比较好,一般用于视频直播相关领域。
使用Go语言的net
包实现的UDP服务端代码以下:
// UDP/server/main.go // UDP server端 func main() { listen, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println("listen failed, err:", err) return } defer listen.Close() for { var data [1024]byte n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据 if err != nil { fmt.Println("read udp failed, err:", err) continue } fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n) _, err = listen.WriteToUDP(data[:n], addr) // 发送数据 if err != nil { fmt.Println("write to udp failed, err:", err) continue } } }
使用Go语言的net
包实现的UDP客户端代码以下:
// UDP 客户端 func main() { socket, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println("链接服务端失败,err:", err) return } defer socket.Close() sendData := []byte("Hello server") _, err = socket.Write(sendData) // 发送数据 if err != nil { fmt.Println("发送数据失败,err:", err) return } data := make([]byte, 4096) n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据 if err != nil { fmt.Println("接收数据失败,err:", err) return } fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n) }