HTTP

参考文章:

  • GitHub:https://github.com/gorilla/websocket

  • Doc:https://godoc.org/github.com/gorilla/websocket

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 方法。

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

1.3 HTTP 状态码

HTTP 状态码的英文为 HTTP Status Code

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

常用HTTP状态码列表:

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

实例:

在这里插入图片描述

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

  • 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中,然后返回

img

什么是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个方法:

type Header map[string][]string

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)
}
net/httphttp.requesthttp.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

net/http
FileServerNotFoundHandlerRedirectHandler

2.2.2 ListenAndServer()

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

func ListenAndServe(addr string, handler Handler) error

该函数有两个参数,第一个参数是自带的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()
}

    http.ListenAndServe("127.1.1.1:8111", nil)

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()Set()Content-LengthContent-typeWrite()htmljsonWriteHeader()http.StatusOK211
// 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 是什么

HTTPWebSocketTCP

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发送的消息类型

TextMessag、BinaryMessage、CloseMessag、PingMessage、PongMessage
TextMessagBinaryMessageCloseMessagePingMessagePongMessagepingpong

3.2.4 控制类消息

ConnWriteControlWriteMessageNextWriter
ConnSetCloseHandlerNextReaderReadMessageRead*CloseError
ConnSetPingHandler 
ConnSetPongHandler 
NextReaderReadMessageRead
Conn

4 gorilla/websocket

gorilla/websocket

4.1 Upgrader

Upgrader 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
}
CheckOringinbooltruefalse

4.1.2 升级协议

func (*Upgrader) Upgrade
// 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)
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
}
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)
   }
}