Golang进阶——网络编程
这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
如果要对Go原生的网络编程跟进一步学习的话,可以参考实战教程:【码神之路】原生Go语言博客实战教程
互联网协议介绍
互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。
这些协议太过庞大和复杂,这里简单介绍一下日常开发中接触较多的几个协议。
互联网分层模型
计网知识中经常提到的OSI七层模型。
互联网的逻辑实现被分为好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感觉到下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。
如上图所示,互联网按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型。
物理层
我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用双绞线、光纤、无线电波等方式。这就叫做”实物理层”,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。
数据链路层
单纯的0和1没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每个信号位有何意义?这就是数据链接层的功能,它在物理层的上方,确定了物理层传输的0和1的分组方式及代表的意义。早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做”以太网”(Ethernet)的协议,占据了主导地位。
以太网规定,一组电信号构成一个数据包,叫做帧(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个字节,总长度不超过65535字节,正好放进一个IP数据包。
UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
应用层
应用程序收到传输层的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的HTTP、HTTPS、FTP、SMTP、POP3、等协议,这些协议就组成了互联网协议的应用。
只使用TCP协议的协议:
HTTP协议:超文本传输协议,用于普通浏览HTTPS协议:安全超文本传输协议,身披SSL外衣的HTTP协议FTP协议:文件传输协议,用于文件传输POP3协议:邮局协议,收邮件使用SMTP协议:简单邮件传输协议,用来发送电子邮件Telent协议:远程登陆协议,通过一个终端登陆到网络SSH协议:安全外壳协议,用于加密安全登陆,替代安全性差的Telent协议
DNS区域传输的时候使用TCP协议,域名解析时使用UDP协议。
发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。
Socket编程
Socket介绍
Socket是BSD UNIX的进程通信机制,通常也称作套接字,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过套接字向网络发出请求或者应答网络请求。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。
Socket又称套接字,应用程序通常通过套接字向网络发出请求或者应答网络请求。常用的Socket类型有两种:流式Socket和数据报式Socket。
- 流式是一种面向连接的Socket, 针对于面向连接的TCP服务应用(TCP:可靠,面向连接,速度慢)
- 数据报式Socket是一种无连接的Socket, 针对于无连接的UDP服务应用(UDP:可靠,无连接,速度快)
举个例子:TCP就像货到付款的快递,送到家还必须见到你人才算一整套流程。UDP就像某快递快递柜一扔就走管你收到收不到,一般直播用UDP。
Go语言实现TCP通信
TCP协议:TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在粘包问题。
TCP服务端
一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。
TCP服务端程序的处理流程:
- 监听端口
- 接收客户端请求建立链接
- 创建goroutine处理链接
TCP服务端代码如下:socket/tcp/server/main.go
package main
import (
"bufio"
"fmt"
"net"
)
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
// 处理函数
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)) // 发送数据
}
}
在终端执行:go build -o ./socket/tcp/cmd/server.exe ./socket/tcp/server,编译出可执行文件server.exe并保存在cmd包下。
这里给出以下项目目录,就清楚
go build的用法了:
TCP客户端
一个TCP客户端进行TCP通信的流程如下:
- 建立与服务端的链接
- 进行数据收发
- 关闭链接
TCP客户端代码如下:socket/tcp/client/client.go
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("建立网络连接出现错误:", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, err := inputReader.ReadString('\n') // 读取用户输入
if err != nil {
fmt.Println("读取用户输入失败:", err)
return
}
inputInfo := strings.Trim(input, "\r\n") // 去掉input中的回车和换行
if inputInfo == "q" || inputInfo == "Q" { // 输入q(Q)就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
fmt.Println("发送数据失败:", err)
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
}
在终端执行:go build -o ./socket/tcp/cmd/client.exe ./socket/tcp/client,编译出可执行文件client.exe并保存在cmd包下。
然后我们先后启动server端和client端,在client端输入任意内容回车之后就能够在server端看到client端发送的数据,从而实现TCP通信。
Go语言实现UDP通信
UDP协议:UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
UDP服务端
UDP服务端代码如下:socket/udp/server/main.go
package main
import (
"fmt"
"net"
)
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", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
在终端执行:go build -o ./socket/udp/cmd/server.exe ./socket/udp/server,编译出可执行文件server.exe并保存在cmd包下。
UDP客户端
UDP服务端代码如下:socket/udp/client/main.go
package main
import (
"fmt"
"net"
)
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,world!")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败!err:", err)
return
}
data := make([]byte, 4096)
n, udpAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败!err:", err)
return
}
fmt.Printf("receive:%v addr:%v count:%v \n", string(data[:n]), udpAddr, n)
}
在终端执行:go build -o ./socket/udp/cmd/client.exe ./socket/udp/client,编译出可执行文件client.exe并保存在cmd包下。
我们先运行server.exe,然后点击一次client.exe都会发现server.exe更新了数据:
TCP粘包
TCP粘包示例
粘包出现的根本原因是不确定消息的边界。接收端在面对 "无边无际"的二进制流的时候,根本不知道收了多少 01 才算一个消息。一不小心拿多了就说是粘包。其实粘包根本不是 TCP 的问题,是使用者对于 TCP 的理解有误导致的一个问题。
只要在发送端每次发送消息的时候给消息带上识别消息边界的信息,接收端就可以根据这些信息识别出消息的边界,从而区分出每个消息。
常见方法有:
- 加入特殊标志
- 加入消息长度信息
我们先看一个例子。
编写服务端代码如下:socket/tcp_stick/server/main.go
package main
import (
"bufio"
"fmt"
"io"
"net"
)
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)
}
}
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
reader := bufio.NewReader(conn)
var buf [1024]byte
for true {
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)
}
}
在终端执行:go build -o ./socket/tcp_stick/cmd/server.exe ./socket/tcp_stick/server,编译出可执行文件server.exe并保存在cmd包下。
编写客户端代码如下:socket/tcp_stick/client/main.go
package main
import (
"fmt"
"net"
)
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,world! --from:qingbo1011.top"
conn.Write([]byte(msg))
}
}
在终端执行:go build -o ./socket/tcp_stick/cmd/client.exe ./socket/tcp_stick/client,编译出可执行文件client.exe并保存在cmd包下。
先启动服务端再启动客户端,可以看到服务端输出结果如下:
客户端分20次发送的数据,在服务端并没有成功的输出20次,而是多条数据“粘”到了一起。
为什么会出现TCP粘包呢?主要原因就是tcp数据传递模式是流模式(传输字节流),在保持长连接的时候可以进行多次的收和发。粘包可发生在发送端也可发生在接收端:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
解决方案
出现粘包的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们先自己定义一个协议,比如数据包的前4个字节为包头,里面存储发送的数据长度。
proto代码如下:socket/tcp_stick/proto/proto.go
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 将消息编码
func Encode(msg string) ([]byte, error) {
// 读取消息的长度,转换为int32类型(占4个字节)
length := int32(len(msg))
pkg := new(bytes.Buffer) // 使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(msg))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, err := reader.Peek(4) // 读取前4个字节的数据
if err != nil {
return "", err
}
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/tcp_stick/server/main.go
package main
import (
"bufio"
"fmt"
"io"
"net"
"web/socket/tcp_stick/proto"
)
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)
}
}
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
reader := bufio.NewReader(conn)
for true {
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)
}
}
客户端代码如下:socket/tcp_stick/client/main.go
package main
import (
"fmt"
"net"
"web/socket/tcp_stick/proto"
)
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,world! --from:qingbo1011.top"
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
conn.Write(data)
}
}
此时再进行go build,然后先运行服务端再运行客户端,结果如下:
这样就解决了TCP粘包问题。
HTTP编程
Web服务器的工作原理可以简单地归纳为:
- 客户机通过TCP/IP协议建立到服务器的TCP连接
- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理动态内容,并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果
HTTP协议:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
HTTP协议承载于TCP协议之上。
HTTP服务端
HTTP服务端代码如下:http/server/main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/go", myHandler) // 单独写回调函数
err := http.ListenAndServe("127.0.0.1:8000", nil) // 参数说明:addr:监听的地址 handler:回调函数
if err != nil {
fmt.Println("err:", err)
return
}
}
// handler函数
func myHandler(writer http.ResponseWriter, request *http.Request) {
fmt.Println(request.RemoteAddr, "连接成功!")
// 请求方式有:GET POST DELETE PUT UPDATE
fmt.Println("method:", request.Method)
// /go路由下
fmt.Println("url:", request.URL)
fmt.Println("header:", request.Header)
fmt.Println("body:", request.Body)
// 回复
writer.Write([]byte("hello,world! form:qingbo1011.top"))
}
HTTP客户端
HTTP客户端代码如下:http/server/main.go
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//resp, err := http.Get("https://www.baidu.com/")
//fmt.Println(resp)
resp, err := http.Get("http://127.0.0.1:8000/go")
if err != nil {
fmt.Println("http.Get Error:", err)
return
}
defer resp.Body.Close() // 关闭连接
fmt.Println(resp.Status)
fmt.Println(resp.Header)
buf := make([]byte, 4096)
for true {
// 接受服务端信息
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("resp.Body.Read Error:", err)
return
}
fmt.Println("读取完毕")
res := string(buf[:n])
fmt.Println(res)
break
}
}
先运行服务端代码,再运行客户端,结果如下:
WebSocket编程
WebSocket是什么
- WebSocket是一种在单个TCP连接上进行全双工通信的协议
- WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
- 在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
需要安装第三方包:cmd中:go get -u -v github.com/gorilla/websocket
聊天室小案例
首先在同一级目录下新建四个go文件:data.go,connection.go,hub.go,server.go。
data.go
编写data.go代码如下:websocket/data.go
package main
type Data struct {
Ip string `json:"ip"`
User string `json:"user"`
From string `json:"from"`
Type string `json:"type"`
Content string `json:"content"`
UserList []string `json:"user_list"`
}
connection.go
编写connection.go代码如下:websocket/connection.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
type connection struct {
ws *websocket.Conn
sc chan []byte
data *Data
}
func (c *connection) writer() {
for msg := range c.sc {
c.ws.WriteMessage(websocket.TextMessage, msg)
}
c.ws.Close()
}
func (c *connection) reader() {
for true {
_, message, err := c.ws.ReadMessage()
if err != nil {
h.r <- c
break
}
json.Unmarshal(message, &c.data) // 反序列化
switch c.data.Type {
case "login":
c.data.User = c.data.Content
c.data.From = c.data.User
user_list = append(user_list, c.data.User)
c.data.UserList = user_list
dataJson, _ := json.Marshal(c.data)
h.b <- dataJson
case "user":
c.data.Type = "user"
dataJson, _ := json.Marshal(c.data)
h.b <- dataJson
case "logout":
c.data.Type = "logout"
user_list = del(user_list, c.data.User)
dataJson, _ := json.Marshal(c.data)
h.b <- dataJson
h.r <- c
default:
fmt.Print("========default================")
}
}
}
var wu = &websocket.Upgrader{
ReadBufferSize: 512,
WriteBufferSize: 512,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
var user_list []string
// 删除切片s中的指定user数据
func del(s []string, user string) []string {
for i, item := range s {
if item == user {
return append(s[:i], s[i+1:]...) // Order matters(如果Order is not important,有更高效的方法)
}
}
return s
}
func myws(w http.ResponseWriter, r *http.Request) {
ws, err := wu.Upgrade(w, r, nil)
if err != nil {
fmt.Println("wu.Upgrade err:", err)
return
}
c := &connection{
ws: ws,
sc: make(chan []byte, 256),
data: &Data{},
}
h.r <- c
go c.writer()
c.reader()
}
hub.go
编写hub.go代码如下:websocket/hub.go
package main
import "encoding/json"
type hub struct {
c map[*connection]bool
b chan []byte
r chan *connection
u chan *connection
}
var h = hub{
c: make(map[*connection]bool),
b: make(chan []byte),
r: make(chan *connection),
u: make(chan *connection),
}
func (h *hub) run() {
for true {
select {
case c := <-h.r:
h.c[c] = true
c.data.Ip = c.ws.RemoteAddr().String()
c.data.Type = "handshake"
c.data.UserList = user_list
dataJson, _ := json.Marshal(c.data)
c.sc <- dataJson
case c := <-h.u:
if _, ok := h.c[c]; ok {
delete(h.c, c)
close(c.sc)
}
case data := <-h.b:
for c := range h.c {
select {
case c.sc <- data:
default:
delete(h.c, c)
close(c.sc)
}
}
}
}
}
server.go
编写server.go代码如下:websocket/server.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
go h.run()
router.HandleFunc("/ws", myws)
err := http.ListenAndServe("127.0.0.1:8080", router)
if err != nil {
fmt.Println("err:", err)
return
}
}
local.html
local.html代码如下:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<style>
p {
text-align: left;
padding-left: 20px;
}
</style>
</head>
<body>
<div style="width: 800px;height: 600px;margin: 30px auto;text-align: center">
<h1>www.5lmh.comy演示聊天室</h1>
<div style="width: 800px;border: 1px solid gray;height: 300px;">
<div style="width: 200px;height: 300px;float: left;text-align: left;">
<p><span>当前在线:</span><span id="user_num">0</span></p>
<div id="user_list" style="overflow: auto;">
</div>
</div>
<div id="msg_list" style="width: 598px;border: 1px solid gray; height: 300px;overflow: scroll;float: left;">
</div>
</div>
<br>
<textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
<input type="button" value="发送" onclick="send()">
</div>
</body>
</html>
<script type="text/javascript">
var uname = prompt('请输入用户名', 'user' + uuid(8, 16));
var ws = new WebSocket("ws://127.0.0.1:8080/ws");
ws.onopen = function () {
var data = "系统消息:建立连接成功";
listMsg(data);
};
ws.onmessage = function (e) {
var msg = JSON.parse(e.data);
var sender, user_name, name_list, change_type;
switch (msg.type) {
case 'system':
sender = '系统消息: ';
break;
case 'user':
sender = msg.from + ': ';
break;
case 'handshake':
var user_info = {'type': 'login', 'content': uname};
sendMsg(user_info);
return;
case 'login':
case 'logout':
user_name = msg.content;
name_list = msg.user_list;
change_type = msg.type;
dealUser(user_name, change_type, name_list);
return;
}
var data = sender + msg.content;
listMsg(data);
};
ws.onerror = function () {
var data = "系统消息 : 出错了,请退出重试.";
listMsg(data);
};
function confirm(event) {
var key_num = event.keyCode;
if (13 == key_num) {
send();
} else {
return false;
}
}
function send() {
var msg_box = document.getElementById("msg_box");
var content = msg_box.value;
var reg = new RegExp("\r\n", "g");
content = content.replace(reg, "");
var msg = {'content': content.trim(), 'type': 'user'};
sendMsg(msg);
msg_box.value = '';
}
function listMsg(data) {
var msg_list = document.getElementById("msg_list");
var msg = document.createElement("p");
msg.innerHTML = data;
msg_list.appendChild(msg);
msg_list.scrollTop = msg_list.scrollHeight;
}
function dealUser(user_name, type, name_list) {
var user_list = document.getElementById("user_list");
var user_num = document.getElementById("user_num");
while(user_list.hasChildNodes()) {
user_list.removeChild(user_list.firstChild);
}
for (var index in name_list) {
var user = document.createElement("p");
user.innerHTML = name_list[index];
user_list.appendChild(user);
}
user_num.innerHTML = name_list.length;
user_list.scrollTop = user_list.scrollHeight;
var change = type == 'login' ? '上线' : '下线';
var data = '系统消息: ' + user_name + ' 已' + change;
listMsg(data);
}
function sendMsg(msg) {
var data = JSON.stringify(msg);
ws.send(data);
}
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
</script>
运行go run websocket/server.go websocket/hub.go websocket/data.go websocket/connection.go,然后在运行local.html文件,结果如下:
爬虫
\