Golang實(shí)現(xiàn)簡單http服務(wù)器的示例詳解
一、基本描述
完成一個http請求的處理和響應(yīng),主要有以下幾個步驟:
- 監(jiān)聽端口
- 建立連接
- 解析http請求
- 處理請求
- 返回http響應(yīng)
完成上面幾個步驟,便能夠?qū)崿F(xiàn)一個簡單的http服務(wù)器,完成對基本的http請求的處理
二 、具體方法
2.1 連接的建立
go中net包下有提供Listen和Accept兩個方法,可以完成連接的建立,可以簡單看下示例:
func main() {
// 對8080端口進(jìn)行監(jiān)聽
l, _ := net.Listen("tcp", ":8080")
// 獲取和端口8080完成三次握手的tcp連接
conn, _ := l.Accept()
// 此時便能夠使用該連接和客戶端進(jìn)行通信
data := make([]byte, 4096)
// 可以從conn讀取數(shù)據(jù)緩沖區(qū)當(dāng)中
conn.Read(data)
// 將緩沖區(qū)的數(shù)據(jù)打印處理
print(string(data))
}可以運(yùn)行這段代碼,然后在瀏覽器對本地8080端口發(fā)送請求,該程序能夠讀取到瀏覽器發(fā)送過來的http請求體數(shù)據(jù)。
當(dāng)通過Accept方法獲取到連接后,能夠使用該連接和客戶端進(jìn)行通信,該連接實(shí)現(xiàn)了net.Conn接口,具體接口的定義如下:
type Conn interface {
// Read reads data from the connection.
// Read can be made to time out and return an error after a fixed
// time limit; see SetDeadline and SetReadDeadline.
Read(b []byte) (n int, err error)
// Write writes data to the connection.
// Write can be made to time out and return an error after a fixed
// time limit; see SetDeadline and SetWriteDeadline.
Write(b []byte) (n int, err error)
// Close closes the connection.
// Any blocked Read or Write operations will be unblocked and return errors.
Close() error
}能夠通過調(diào)用Read方法從客戶端讀取數(shù)據(jù),使用Write方法往客戶端返回?cái)?shù)據(jù)。
2.2 http請求解析
當(dāng)和客戶端建立連接后,同時也能夠讀取到客戶端發(fā)送過來的請求,此時要處理http請求的話,此時是需要解析出http請求體的,然后才能對http請求進(jìn)行處理。接下來我們看一下一個http請求例子:
GET /ping HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
\r\n(空行)
hello world
接下來對HTTP請求體來進(jìn)行分析,第一行是請求行,包含請求的方法,請求URI,以及HTTP版本。下面這個例子中,請求方法是GET,請求URI是/ping,HTTP版本是1.1。
GET /ping HTTP/1.1
請求行到空行之間的內(nèi)容便是請求頭部,每一個頭部字段都有其對應(yīng)的作用,比如Connection首部字段,這里值為keep-alive,這里的作用是告訴服務(wù)器,這個連接要處理多個http請求,不要處理完一個http請求就把連接斷開了。
而且一個http請求首部字段,是可以有多個對應(yīng)的值的,多個值之間用逗號隔開。
Cache-Control: public, max-age=31536000
第三部分的內(nèi)容為請求體,也就是空行之后直到整個http請求的結(jié)束??梢钥聪旅胬?,請求體的內(nèi)容是hello world。實(shí)際上GET請求是不應(yīng)該有請求體的內(nèi)容的,此處是我手動加進(jìn)去的,只是為了方便展示使用。
GET /ping HTTP/1.1
....(省略http請求體部分首部字段)
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
\r\n(空行)
hello world
當(dāng)我們了解完http請求體的結(jié)構(gòu),接下來可以編寫代碼來解析http請求體。
我們定義一個Conn結(jié)構(gòu)體,由Conn完成數(shù)據(jù)的讀取和HTTP請求體的解析,Conn定義如下:
type Conn struct {
rwc net.Conn
// bufio.Reader 提供了緩存的功能,以及一些函數(shù),方便我們解析HTTP請求體
br *bufio.Reader
bw *bufio.Writer
// 是否已經(jīng)寫入響應(yīng)頭
wroteHaeder bool
}
func NewConn(rwc net.Conn) *Conn {
return &Conn{
rwc: rwc,
br: bufio.NewReader(rwc),
bw: bufio.NewWriter(rwc),
}
}同時解析出來的HTTP請求,也需要有個結(jié)構(gòu)體來存儲這部分?jǐn)?shù)據(jù),Request定義如下,這里暫時只支持GET請求,所以并沒有保存請求體的內(nèi)容
type Request struct {
// 存儲請求方法,上面例子便是GET
method string
// 用于存儲請求的uri
uri string
// 用于存儲http版本
proto string
// http首部字段
Header map[string]string
}接下來由Conn完成HTTP請求體的解析,然后將解析的結(jié)果存儲到Request對象當(dāng)中。只需要根據(jù)HTTP請求體結(jié)構(gòu)來進(jìn)行解析即可,具體邏輯如下:
func (c *Conn) readRequest() (*Request, error) {
req := NewRequest()
// 現(xiàn)在只支持Get請求,讀取第一行內(nèi)容
// GET /ping HTTP1.1
line, err := c.br.ReadBytes('\n')
if err != nil {
return req, err
}
var f []string
// 按空格來進(jìn)行分割,將請求行分割為三部分
if f = strings.Split(string(line), " "); len(f) != 3 {
return req, errors.New("http Header error")
}
// 獲取到GET, /ping, HTTP/1.1
req.method, req.url, req.proto = f[0], f[1], f[2]
// 解析請求體首部字段
for {
line, err = c.br.ReadBytes('\n')
if err != nil {
return nil, err
}
// 當(dāng)讀取到空行時,說明已經(jīng)首部字段已經(jīng)讀取完了
if len(strings.TrimSpace(string(line))) == 0 {
break
}
//舉例,讀取connection: keep-alive,獲取第一個空格的下標(biāo)
i := bytes.IndexByte(line, ' ')
if i == -1 {
return nil, errors.New("header is error")
}
// 此時獲取到請求首部key,為connection
key := string(line[:i-1])
// 讀取到對應(yīng)的值,這里讀取到keep-alive
value := strings.TrimSpace(string(line[i:]))
// 簡單讀取頭部字段即可
req.Header[key] = value
}
}2.3 http請求處理
此時已經(jīng)獲取到HTTP請求了,之后需要對HTTP請求來進(jìn)行處理,這里可以先簡單進(jìn)行處理,根據(jù)不同的請求執(zhí)行不同的處理邏輯:
func main() {
// 對8080端口進(jìn)行監(jiān)聽
l, _ := net.Listen("tcp", ":8080")
// 獲取和端口8080完成三次握手的tcp連接
conn, _ := l.Accept()
// 獲取到conn連接
c := NewConn(conn, handler)
// 讀取到請求體
req, _ := c.readRequest()
if request.uri == "/hello" {
....
}else{
....
}
}2.4 http請求響應(yīng)
當(dāng)http請求處理完成之后,需要將返回一個處理結(jié)果返回給客戶端,有時候還需要返回一些數(shù)據(jù)給客戶端,這里返回的數(shù)據(jù)需要符合HTTP響應(yīng)體的結(jié)構(gòu),接下來我們看看HTTP響應(yīng)體的結(jié)構(gòu)
HTTP/1.1 200 OK
Server: CloudWAF
Date: Sun, 04 Dec 2022 02:29:27 GMT
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Content-Language: zh-CN
Strict-Transport-Security: max-age= 31536000
Content-Encoding: gzip
\r\n(空行)
xxxx響應(yīng)數(shù)據(jù)
可以看到,HTTP響應(yīng)體和請求體結(jié)構(gòu)類似,當(dāng)需要返回?cái)?shù)據(jù)給客戶端時,需要按照HTTP協(xié)議定義好的響應(yīng)體結(jié)構(gòu)來進(jìn)行返回,這樣客戶端才能夠正確解析。
為了方便使用,構(gòu)造HTTP響應(yīng)體結(jié)構(gòu)這部分邏輯應(yīng)該由Conn對象來承載,由Conn對象提供一個Write方法,當(dāng)需要返回?cái)?shù)據(jù)時,只需要調(diào)用Write方法寫入要返回的數(shù)據(jù)即可,不需要去操心去構(gòu)造HTTP響應(yīng)體的內(nèi)容,Writer方法具體邏輯如下:
const (
StatusOK = 200
)
var statusText = map[int]string{
StatusOK: "OK",
}
// 返回響應(yīng)行
// 構(gòu)造響應(yīng)行 HTTP/1.1 200 OK
func (c *Conn) writeHeader(status int) error {
if c.wroteHeader {
return errors.New("code has been set")
}
c.wroteHeader = true
var proto string
//GET /hello HTTP/1.1
proto = "HTTP/1.1"
// 獲取文本描述,這里為OK
text, ok := statusText[status]
if !ok {
text = "status code " + strconv.Itoa(status)
}
// 寫入數(shù)據(jù) HTTP1.1 200 OK\r\n
c.bw.WriteString(proto + " " + strconv.Itoa(status) + " " + text + "\r\n")
// 寫入空行
c.bw.Write("\r\n")
return nil
}
// 寫入響應(yīng)數(shù)據(jù)
func (c *Conn) WriteData(data []byte) error {
// 還沒寫入請求頭
if !c.wroteHeader {
//默認(rèn)狀態(tài)碼是200 OK
c.writeHeader(StatusOK)
}
c.bw.Write(data)
return nil
}三、完整示例
func main() {
// 對8080端口進(jìn)行監(jiān)聽
l, _ := net.Listen("tcp", ":8080")
// 獲取和端口8080完成三次握手的tcp連接
conn, _ := l.Accept()
// 獲取到conn連接
c := NewConn(conn)
// 讀取到請求體
req, _ := c.readRequest()
if request.uri == "hello" {
c.WriteData("hello")
}else{
c.WriteData("hello world")
}
// 在最后,需要將緩沖區(qū)的數(shù)據(jù)進(jìn)行清空
c.bw.Flush()
// 因?yàn)轫憫?yīng)沒有設(shè)置content-length,所以只有連接斷開后,
// 瀏覽器才知道數(shù)據(jù)讀取完成了,此處需要斷開連接
c.rwc.Close()
}到此為止,一個簡單的HTTP服務(wù)器已經(jīng)實(shí)現(xiàn)了,能夠?qū)崿F(xiàn)簡單的HTTP請求的解析和響應(yīng)。
以上就是Golang實(shí)現(xiàn)簡單http服務(wù)器的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang http服務(wù)器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
GoLang中panic與recover函數(shù)以及defer語句超詳細(xì)講解
這篇文章主要介紹了GoLang的panic、recover函數(shù),以及defer語句,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01
golang項(xiàng)目如何上線部署到Linu服務(wù)器(方法詳解)
這篇文章主要介紹了golang項(xiàng)目如何上線部署到Linu服務(wù)器,本文通過兩種方法給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10

