天天看点

go语言webSocket框架——gorillaHTTP

文章目录

  • HTTP
    • 参考文章:
    • 1 HTTP协议
      • 1.1 是什么?
        • 1.1.1 客户端请求消息
        • 1.1.2 服务器响应消息
      • 1.2 HTTP 请求方法
      • 1.3 HTTP 状态码
      • 1.4 content-type 内容类型
    • 2 http.HandleFunc
      • 2.1 是什么
        • 2.1.1 ResponseWriter接口
      • 2.2 简单实现
        • 2.2.1 net/http 提供的handler
        • 2.2.2 ListenAndServer()
        • 2.2.3 Request
        • 2.2.4 ResponseWriter
      • 2.3 test
        • 2.3.1 HTTP服务端
        • 2.3.2 HTTP客户端
    • 3 webSocket
      • 3.1 是什么
      • 3.2 webSocket握手协议
        • 3.2.1 客户端请求 Request Header
        • 3.2.2 服务器响应 Response Header
        • 3.2.3 websocket发送的消息类型
        • 3.2.4 控制类消息
    • 4 gorilla/websocket
      • 4.1 Upgrader
        • 4.1.1 创建Upgrader实例
        • 4.1.2 升级协议
        • 4.1.3 设置关闭连接监听
        • 4.1.4 总览
    • 5 Demo
      • 5.1
        • 5.1.1 服务端
        • 5.1.2 客户端
        • 5.1.3 简单版

HTTP

参考文章:

  • GitHub:https://github.com/gorilla/websocket
  • Doc:https://godoc.org/github.com/gorilla/websocket
  • Go Web实现
  • gorilla/websocket使用教程
  • go http服务器编程

1 HTTP协议

1.1 是什么?

HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)

  • HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。
  • 一个HTTP"客户端"是一个应用程序(Web浏览器或其他任何客户端),连接到服务器达到向服务器发送一个或多个HTTP的请求。
  • 一个HTTP"服务器"同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。
  • HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

1.1.1 客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1X0mAnbT-1661397084862)(https://atts.w3cschool.cn/attachments/image/21161225/1456372149731272.png)]

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/1.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
           

1.1.2 服务器响应消息

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-462SsJTz-1661397084863)(https://atts.w3cschool.cn/attachments/image/21161225/1456372149775233.jpg)]

# 状态行
HTTP/1.1 211 OK			
# 响应头
Date: Mon, 27 Jul 2119 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2119 19:15:56 GMT
ETag: "34aa387-d-1568eb11"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
           

1.2 HTTP 请求方法

HTTP1.1 定义了三种请求方法: GET, POST 和 HEAD 方法。

HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。

序号 方法 描述
1 GET 请求指定的页面信息,并返回实体主体。
2 HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
5 DELETE 请求服务器删除指定的页面。
6 CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
7 OPTIONS 允许客户端查看服务器的性能。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。
9 PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新 。

1.3 HTTP 状态码

HTTP 状态码的英文为 HTTP Status Code

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

常用HTTP状态码列表:

状态码 状态码英文名称 中文描述
211 OK 请求成功。一般用于GET与POST请求
311 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。
315 Use Proxy 使用代理。所请求的资源必须通过代理访问
411 Bad Request 客户端请求的语法错误,服务器无法理解
411 Unauthorized 请求要求用户的身份认证
413 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
414 Not Found 服务器无法根据客户端的请求找到资源(网页)。
511 Internal Server Error 服务器内部错误,无法完成请求
512 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
514 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求

1.4 content-type 内容类型

参考:https://www.runoob.com/http/http-content-type.html

Content-Type 标头告诉客户端实际返回的内容的内容类型。用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件。

语法格式:

Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something
           

实例:

go语言webSocket框架——gorillaHTTP

常见的媒体格式类型如下:

  • text/html : HTML格式
  • text/plain :纯文本格式
  • text/xml : XML格式
  • image/gif :gif图片格式
  • image/jpeg :jpg图片格式
  • image/png:png图片格式

以application开头的媒体格式类型:

  • application/xhtml+xml :XHTML格式
  • application/xml: XML数据格式
  • application/atom+xml :Atom XML聚合格式
  • application/json: JSON数据格式
  • application/pdf:pdf格式
  • application/msword : Word文档格式
  • application/octet-stream : 二进制流数据(如常见的文件下载)
  • application/x-www-form-urlencoded : 中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)

另外一种常见的媒体格式是上传文件之时使用的:

  • multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

2 http.HandleFunc

参考:https://www.cnblogs.com/f-ck-need-u/p/11121951.html

2.1 是什么

Multiplexer根据URL将请求路由给指定的Handler。Handler用于处理请求并给予响应。更严格地说,用来读取请求体、并将请求对应的响应字段(respones header)写入ResponseWriter中,然后返回

go语言webSocket框架——gorillaHTTP

什么是Handler。它是一个接口,定义在net/http/server.go中:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
           

也就是说,实现了ServerHTTP方法的都是Handler。注意ServerHTTP方法的参数:http.ResponesWriter接口和Request指针。

在Handler的注释中,给出了几点主要的说明:

  • Handler用于响应一个HTTP request
  • 接口方法ServerHTTP应该用来将response header和需要响应的数据写入到ResponseWriter中,然后返回。

2.1.1 ResponseWriter接口

ResponseWriter接口的作用是用于构造HTTP response,并将响应header和响应数据通过网络链接发送给客户端。

// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
//
// A ResponseWriter may not be used after the Handler.ServeHTTP method
// has returned.
type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}
           

这个接口有3个方法:

  • Header()方法,用来构造响应Header,它返回一个Header对象,这个Header对象稍后将被WriterHeader()响应出去。Header类型是一个map类型的结构,字段名为key、字段值为value:
  • Write()方法用于向网络连接中写响应数据。
  • WriteHeader()方法将给定的响应状态码和响应Header一起发送出去。

Go有一个函数HandleFunc(),它表示使用第二个参数的函数作为handler,处理匹配到的url路径请求。HandleFunc 的第一个参数指的是请求路径,第二个参数是一个函数类型,表示这个请求需要处理的事情。没有处理复杂的逻辑,而是直接给DefaultServeMux处理,如源码:

参考:https://blog.51cto.com/u_8184160/2052853

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
           

golang 的标准库

net/http

提供了 http 编程有关的接口,封装了内部TCP连接和报文解析的复杂琐碎的细节,使用者只需要和

http.request

http.ResponseWriter

两个对象交互。

源码,相当于一个适配器

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
           

2.2 简单实现

package main

import (
   "io"
   "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
   io.WriteString(w, "hello world!\n")
}

func main() {
    //  回调函数
   http.HandleFunc("/", helloHandler)
   http.ListenAndServe(":8181", nil)
}
           

2.2.1 net/http 提供的handler

大部分的服务器逻辑都需要使用者编写对应的 Handler,不过有些 Handler 使用频繁,因此

net/http

提供了它们的实现。

  • 比如负责文件 hosting 的

    FileServer

  • 负责 414 的

    NotFoundHandler

  • 负责重定向的

    RedirectHandler

2.2.2 ListenAndServer()

在启动go http自带的web服务时,调用了函数ListenAndServe()。这个函数的定义如下:

该函数有两个参数,第一个参数是自带的web监听地址和端口,第二个参数是Handler,用来处理每个接进来的http request,但一般第二个参数设置为nil,表示调用默认的Multiplexer:DefaultServeMux。这个默认的ServeMux唯一的作用,是将请求根据URL路由给对应的handler进行处理。

var DefaultServeMux = &defaultServeMux
           
// addr:监听的地址
// handler:回调函数
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

           

2.2.3 Request

Request 就是封装好的客户端请求,包括 URL,method,header 等等所有信息,以及一些方便使用的方法:

Handler 需要知道关于请求的任何信息,都要从这个对象中获取,一般不会直接修改这个对象

type Request struct {
    // Method specifies the HTTP method (GET, POST, PUT, etc.).For client requests an empty string means GET.
    Method string

    // URL specifies either the URI being requested (for server requests) or the URL to access (for client requests).
    URL *url.URL

    // The protocol version for incoming requests.
    // Client requests always use HTTP/1.1.
    Proto      string // "HTTP/1.1"
    ProtoMajor int    // 1
    ProtoMinor int    // 1

    // A header maps request lines to their values.
    // If the header says
    //
    //    accept-encoding: gzip, deflate
    //    Accept-Language: en-us
    //    Connection: keep-alive
    //
    // then
    //
    //    Header = map[string][]string{
    //        "Accept-Encoding": {"gzip, deflate"},
    //        "Accept-Language": {"en-us"},
    //        "Connection": {"keep-alive"},
    //    }
    Header Header

    // Body is the request's body.
    Body io.ReadCloser
    ContentLength int64
    TransferEncoding []string
    Close bool
    Host string
    Form url.Values
    PostForm url.Values
    MultipartForm *multipart.Form
    ...
    RemoteAddr string
    ...
}
           

2.2.4 ResponseWriter

ResponseWriter 是一个接口,定义了三个方法:

  • Header()

    :返回一个 Header 对象,可以通过它的

    Set()

    方法设置头部,注意最终返回的头部信息可能和你写进去的不完全相同,因为后续处理还可能修改头部的值(比如设置

    Content-Length

    Content-type

    等操作)
  • Write()

    : 写 response 的主体部分,比如

    html

    或者

    json

    的内容就是放到这里的
  • WriteHeader()

    :设置 status code,如果没有调用这个函数,默认设置为

    http.StatusOK

    , 就是

    211

    状态码
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(int)
}
           

2.3 test

2.3.1 HTTP服务端

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/go", myHandler)
    http.ListenAndServe("127.1.1.1:8111", nil)
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.RemoteAddr, "连接成功")
    // 请求方式:GET POST DELETE PUT UPDATE
    fmt.Println("method:", r.Method)
    fmt.Println("url:", r.URL.Path)
    fmt.Println("header:", r.Header)
    fmt.Println("body:", r.Body)
    // 回复
    w.Write([]byte("test成功"))
}
           

2.3.2 HTTP客户端

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    //resp, _ := http.Get("http://www.baidu.com")
    //fmt.Println(resp)
    resp, _ := http.Get("http://127.1.1.1:8111/go")
    defer resp.Body.Close()
    // 211 OK
    fmt.Println(resp.Status)
    fmt.Println(resp.Header)

    buf := make([]byte, 1124)
    for {
        // 接收服务端信息
        n, err := resp.Body.Read(buf)
        if err != nil && err != io.EOF {
            fmt.Println(err)
            return
        } else {
            fmt.Println("读取完毕")
            res := string(buf[:n])
            fmt.Println(res)
            break
        }
    }
}
           

3 webSocket

3.1 是什么

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议,长连接,双向传输
  • 需要安装第三方包:go get -u -v github.com/gorilla/websocket
  • WebSocket 协议实现起来相对简单。

    HTTP

    协议初始握手建立连接,

    WebSocket

    实质上使用原始

    TCP

    读取 / 写入数据
  • http有良好的兼容性,ws和http的默认端口都是81,wss和https的默认端口都是443

3.2 webSocket握手协议

3.2.1 客户端请求 Request Header

GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket   	// 指明使用WebSocket协议
    Connection: Upgrade		// 指明使用WebSocket协议
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==   // Bse64 encode的值,是浏览器随机生成的
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13  //指定Websocket协议版本
    Origin: http://example.com
           

服务端收到Sec-WebSocket-Key后拼接上一个固定的GUID,进行一次SHA-1摘要,再转成Base64编码,得到Sec-WebSocket-Accept返回给客户端。客户端对本地的Sec-WebSocket-Key执行同样的操作跟服务端返回的结果进行对比,如果不一致会返回错误关闭连接。如此操作是为了把websocket header 跟http header区分开

3.2.2 服务器响应 Response Header

HTTP/1.1 111 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc1sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat
           

3.2.3 websocket发送的消息类型

5种:

TextMessag、BinaryMessage、CloseMessag、PingMessage、PongMessage

  • TextMessag

    BinaryMessage

    分别表示发送文本消息和二进制消息
  • CloseMessage

    关闭帧,接收方收到这个消息就关闭连接
  • PingMessage

    PongMessage

    是保持心跳的帧,服务器发

    ping

    给浏览器,浏览器返回

    pong

    消息

3.2.4 控制类消息

Websocket协议定义了三种控制消息:Close、Ping和Pong。通过调用

Conn

WriteControl

WriteMessage

NextWriter

方法向对端发送控制消息。

Conn

收到了Close消息之后,调用由

SetCloseHandler

方法设置的handler函数,然后从

NextReader

ReadMessage

或消息的

Read

方法返回一个

*CloseError

。缺省的close handler会发送一个Close消息到对端。

Conn

收到了Ping消息之后,调用由

SetPingHandler

方法设置的handler函数。缺省的ping handler会发送一个Pong消息到对象。

Conn

收到了Pong消息之后,调用由

SetPongHandler

设置的handler函数。缺省的pong handler什么也不做。

控制消息的handler函数是从

NextReader

ReadMessage

和消息的

Read

方法中调用的。缺省的close handler和ping handler向对端写数据时可能会短暂阻塞这些方法。

应用程序必须读取

Conn

,使得对端发送的close、ping、和pong消息能够得到处理。即使应用程序不关心对端发送的消息,也应该启动一个goroutine来读取对端的消息并丢弃。例如:

4 gorilla/websocket

websocket由http升级而来,首先发送附带Upgrade请求头的Http请求,所以我们需要在处理Http请求时拦截请求并判断其是否为websocket升级请求,如果是则调用

gorilla/websocket

库相应函数处理升级请求

4.1 Upgrader

Upgrader

发送附带Upgrade请求头的Http请求,把 http 请求升级为长连接的

WebSocket

,结构如下:

type Upgrader struct {
    // 升级 websocket 握手完成的超时时间
    HandshakeTimeout time.Duration

    // io 操作的缓存大小,如果不指定就会自动分配。
    ReadBufferSize, WriteBufferSize int

    // 写数据操作的缓存池,如果没有设置值,write buffers 将会分配到链接生命周期里。
    WriteBufferPool BufferPool

    //按顺序指定服务支持的协议,如值存在,则服务会从第一个开始匹配客户端的协议。
    Subprotocols []string

    // http 的错误响应函数,如果没有设置 Error 则,会生成 http.Error 的错误响应。
    Error func(w http.ResponseWriter, r *http.Request, status int, reason error)

    // 如果请求Origin标头可以接受,CheckOrigin将返回true。 如果CheckOrigin为nil,则使用安全默认值:如果Origin请求头存在且原始主机不等于请求主机头,则返回false。
    // 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造。如果不检查,就设置一个返回值为true的函数
    CheckOrigin func(r *http.Request) bool

    // EnableCompression 指定服务器是否应尝试协商每个邮件压缩(RFC 7692)。 将此值设置为true并不能保证将支持压缩。 目前仅支持“无上下文接管”模式
    EnableCompression bool
}
           

4.1.1 创建Upgrader实例

该实例用于升级请求

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1124, //指定读缓存大小
    WriteBufferSize: 1124, //指定写缓存大小
    CheckOrigin:     checkOrigin,
}
// 检测请求来源
func checkOrigin(r *http.Request) bool {
    if r.Method != "GET" {
        fmt.Println("method is not GET")
        return false
    }
    if r.URL.Path != "/ws" {
        fmt.Println("path error")
        return false
    }
    return true
}
           

其中

CheckOringin

是一个函数,该函数用于拦截或放行跨域请求。函数返回值为

bool

类型,即

true

放行,

false

拦截。如果请求不是跨域请求可以不赋值

4.1.2 升级协议

func (*Upgrader) Upgrade

函数将 http 升级到 WebSocket 协议。

// responseHeader包含在对客户端升级请求的响应中。 
// 使用responseHeader指定cookie(Set-Cookie)和应用程序协商的子协议(Sec-WebSocket-Protocol)。
// 如果升级失败,则升级将使用HTTP错误响应回复客户端
// 返回一个 Conn 指针,使用 Conn 读写数据与客户端通信。
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error)
           

升级为websocket连接并获得一个conn实例,之后的发送接收操作皆有conn,其类型为websocket.Conn。

//Http入口
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    //判断请求是否为websocket升级请求。
    if websocket.IsWebSocketUpgrade(r) {
        // 收到 http 请求后升级协议
        conn, err := upgrader.Upgrade(w, r, w.Header())
        // 向客户端发送消息使用 WriteMessage(messageType int, data []byte),参数1为消息类型,参数2消息内容
        conn.WriteMessage(websocket.TextMessage, []byte("升级成功"))
        // 接受客户端消息使用 ReadMessage(),该操作阻塞线程所以建议运行在其他协程上。
        //返回值(接收消息类型、接收消息内容、发生的错误)当然正常执行时错误为 nil。一旦连接关闭返回值类型为-1可用来终止读操作。
        go func() {
            for {
                t, c, _ := conn.ReadMessage()
                fmt.Println(t, string(c))
                if t == -1 {
                    return
                }
            }
        }()
    } else {
        //处理普通请求
        c := newContext(w, r)
        e.router.handle(c)
    }
}
           

4.1.3 设置关闭连接监听

函数为

SetCloseHandler(h func(code int, text string) error)

函数接收一个函数为参数,参数为nil时有一个默认实现,其源码为:

func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
    if h == nil {
        h = func(code int, text string) error {
            message := FormatCloseMessage(code, "")
            c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
            return nil
        }
    }
    c.handleClose = h
}
           

可以看到作为参数的函数的参数为int和string类型正好和前端的close(long string)对应即前端调用close(long string)关闭连接后两个参数会被发送给后端并最终被

func(code int, text string) error

所使用。

// 设置关闭连接监听
conn.SetCloseHandler(func(code int, text string) error {
    fmt.Println(code, text) // 断开连接时将打印code和text
    return nil
})
           

4.1.4 总览

type WsServer struct {
    ......
    // 定义一个 upgrade 类型用于升级 http 为 websocket
    upgrade  *websocket.Upgrader
}

func NewWsServer() *WsServer {
    ws.upgrade = &websocket.Upgrader{
        ReadBufferSize:  4196,//指定读缓存区大小
        WriteBufferSize: 1124,// 指定写缓存区大小
        // 检测请求来源
        CheckOrigin: func(r *http.Request) bool {
            if r.Method != "GET" {
                fmt.Println("method is not GET")
                return false
            }
            if r.URL.Path != "/ws" {
                fmt.Println("path error")
                return false
            }
            return true
        },upgrade
    }
    return ws
}

func (self *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ......
    // 收到 http 请求后 升级 协议
    conn, err := self.upgrade.Upgrade(w, r, nil)
    if err != nil {
        fmt.Println("websocket error:", err)
        return
    }
    fmt.Println("client connect :", conn.RemoteAddr())
    go self.connHandle(conn)

}
           

5 Demo

5.1

5.1.1 服务端

package main

import (
	"fmt"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  4196,
	WriteBufferSize: 1124,
	CheckOrigin: func(r *http.Request) bool {

		//if r.Method != "GET" {
		//	fmt.Println("method is not GET")
		//	return false
		//}
		//if r.URL.Path != "/ws" {
		//	fmt.Println("path error")
		//	return false
		//}
		return true
	},
}

// ServerHTTP 用于升级协议
func ServerHTTP(w http.ResponseWriter, r *http.Request) {
	// 收到http请求之后升级协议
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Error during connection upgrade:", err)
		return
	}
	defer conn.Close()

	for {
		// 服务端读取客户端请求
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("Error during message reading:", err)
			break
		}
		log.Printf("Received:%s", message)

		// 开启关闭连接监听
		conn.SetCloseHandler(func(code int, text string) error {
			fmt.Println(code, text) // 断开连接时将打印code和text
			return nil
		})

		//服务端给客户端返回请求
		err = conn.WriteMessage(messageType, message)
		if err != nil {
			log.Println("Error during message writing:", err)
			return
		}

	}
}

func home(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Index Page")
}

func main() {
	http.HandleFunc("/socket", ServerHTTP)
	http.HandleFunc("/", home)
	log.Fatal(http.ListenAndServe("localhost:8181", nil))
}

           

5.1.2 客户端

// client.go
package main

import (
	"github.com/gorilla/websocket"
	"log"
	"os"
	"os/signal"
	"time"
)

var done chan interface{}
var interrupt chan os.Signal

func receiveHandler(connection *websocket.Conn) {
	defer close(done)
	for {
		_, msg, err := connection.ReadMessage()
		if err != nil {
			log.Println("Error in receive:", err)
			return
		}
		log.Printf("Received: %s\n", msg)
	}
}

func main() {
	done = make(chan interface{})    // Channel to indicate that the receiverHandler is done
	interrupt = make(chan os.Signal) // Channel to listen for interrupt signal to terminate gracefully

	signal.Notify(interrupt, os.Interrupt) // Notify the interrupt channel for SIGINT

	socketUrl := "ws://localhost:8181" + "/socket"
	conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
	if err != nil {
		log.Fatal("Error connecting to Websocket Server:", err)
	}
	defer conn.Close()
	go receiveHandler(conn)

	// 无限循环使用select来通过通道监听事件
	for {
		select {
		case <-time.After(time.Duration(1) * time.Millisecond * 1111):
			//conn.WriteMessage()每秒钟写一条消息
			err := conn.WriteMessage(websocket.TextMessage, []byte("Hello from GolangDocs!"))
			if err != nil {
				log.Println("Error during writing to websocket:", err)
				return
			}
		//如果激活了中断信号,则所有未决的连接都将关闭
		case <-interrupt:
			// We received a SIGINT (Ctrl + C). Terminate gracefully...
			log.Println("Received SIGINT interrupt signal. Closing all pending connections")

			// Close our websocket connection
			err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
			if err != nil {
				log.Println("Error during closing websocket:", err)
				return
			}

			select {
			// 如果receiveHandler通道退出,则通道'done'将关闭
			case <-done:
				log.Println("Receiver Channel Closed! Exiting....")
			//如果'done'通道未关闭,则在1秒钟后会有超时,因此程序将在1秒钟超时后退出
			case <-time.After(time.Duration(1) * time.Second):
				log.Println("Timeout in closing receiving channel. Exiting....")
			}
			return
		}
	}
}

           

5.1.3 简单版

服务端

package main

import (
	"github.com/gorilla/websocket"
	"log"
	"net/http"
)

var upgrade = websocket.Upgrader{
	ReadBufferSize:  1124,
	WriteBufferSize: 1124,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func HelloHTTP(w http.ResponseWriter, r *http.Request) {
	//1.升级协议,并返回升级后的长连接
	conn, err := upgrade.Upgrade(w, r, nil)

	if err != nil {
		log.Println("Error during connection upgrade:", err)
		return
	}
	defer conn.Close()

	for {
		// 2.读取客户端的请求信息
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("Error during message writing:", err)
			return
		}
		log.Printf("Recive message:%s", message)
		//	3.返回给客户端信息
		err = conn.WriteMessage(messageType, message)
		if err != nil {
			log.Println("Error during message writing:", err)
			return
		}
	}

}

func main() {

	http.HandleFunc("/socket", HelloHTTP)
	http.ListenAndServe(":8181", nil)
}

           

客户端

package main

import (
   "github.com/gorilla/websocket"
   "log"
   "time"
)

func ReceiveHandler(con *websocket.Conn) {
   for {
      _, message, err := con.ReadMessage()
      if err != nil {
         log.Println("Error during Receive:", err)
         return
      }
      log.Printf("Receive:%s\n", message)
   }
}

func main() {
   socketUrl := "ws://localhost:8181" + "/socket"
   // 使用 net.Dialer Dialer.Dial 函数建立 TCP 连接,建立成功后,取得了 net.Conn 对象,
   conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
   if err != nil {
      log.Fatal("Error connecting to websocket Server:", err)
   }
   defer conn.Close()

   ticker := time.Tick(time.Second)
   for range ticker {
      err = conn.WriteMessage(websocket.TextMessage, []byte("Hello World!"))
      if err != nil {
         log.Println("Error during writing to websocket:", err)
         return
      }
      // 接受客户端消息使用ReadMessage()该操作会阻塞线程所以建议运行在其他协程上
      go ReceiveHandler(conn)
   }
}