Golang 学习笔记:HTTP, TCP/IP, UDP

HTTP

我们知道,go内置有http server 支持,我们只需要在代码中启动这个server就可以启动一个类似于apache的服务器。同时可以很方便的监听多个端口等等。

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

const (
	PORT = ":1024"
	MSG = "hello, gopher"
)
func main() {
	http.HandleFunc("/hello", Hello)
	go func() {
		http.ListenAndServe(PORT, nil)
	}()
	
	go func() {
		http.ListenAndServeTLS(":443","cert.pem", "key.pem", nil)
	}()
	for {}
}
func Hello(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	Fprintf(w, MSG)
}

这段代码启动了一个server,开启了http和https支持;分别监听在1024端口和443端口。
有几个需要注意的地方。

  1. http.HandleFunc(string, func(http.ResponseWriter, *http.Request)) 绑定某个请求路径的处理方法
  2. 使用goroutine执行监听与服务动作。启动了两个goroutine
  3. 最后使用一个for循环保证主线程不会退出。因为主线程退出的话goroutine也会结束。
  4. 其中的TLS支持的HTTPS服务的cert和key文件可以使用Go源码中带的文件生成go run $GOROOT/src/crypto/tls/generate_cert.go -ca=true -host="localhost"

最后一个无限循环可以使用channel代替,可以使用sync.WaitGroup实现, sync.WaitGroup持有一个计数器,其Add(int)方法增加计数器,其Done()方法减小计数器,其Wait()方法会让线程阻塞等待计数器恢复0。其实现如下。

import (
	. "fmt"
	"net/http"
	"sync"
)

const (
	PORT = ":1024"
	MSG = "hello, gopher"
)
var servers sync.WaitGroup

func main() {
	http.HandleFunc("/hello", Hello)
	Launch(func() {
		http.ListenAndServe(PORT, nil)
	})
	
	Launch(func() {
		http.ListenAndServeTLS(":443","cert.pem", "key.pem", nil)
	})
	
	servers.Wait()
}

func Launch(f func()) {
	servers.Add(1)
	go func() {
		defer servers.Done()
		f()
	}()
}

func Hello(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	Fprintf(w, MSG)
}

使用上面的方法可以添加任意多的监听端口。可以通过Server做更多的配置。

s := &http.Server{
	Addr:           ":8080",
	Handler:        myHandler,
	ReadTimeout:    10 * time.Second,
	WriteTimeout:   10 * time.Second,
	MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

HTTP与Go程结合

我们对Goroutine和HTTP都比较熟悉了,我们可以设置一个任务队列,让每一个请求都进入这个队列然后使用一定数量的Goroutine去执行逻辑。这样效率比上面的实现要高很多。

发送GET/POST请求

http保重提供了非常方便的发发送get请求和post请求的封装。

resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
	url.Values{"key": {"Value"}, "id": {"123"}})

defer resp.Body.Close()  // 不要忘记defer关闭资源
body, err := ioutil.ReadAll(resp.Body)  // 读取资源
strCont := string(body)

环境变量

使用os包中的Getenv(string)Setenv(string, string)方法获得和设置环境变量。要注意Setenv的作用域只是当前运行进程,结束后或者重新运行后环境变量会重置为系统设置值。

Println(os.Getenv("PATH"))
os.Setenv("GOOO", "gofast")
os.Setenv("PATH", ".")
Println(os.Getenv("PATH"))

操作系统信号

当程序在命令行中运行的时候,一般使用Ctrl+c可以终结程序运行。对于操作系统有一定了解的话应该知道,Ctrl+c向运行的进程发送了一个结束进程的signal。这种方式很有用,但是有点时候暴力结束进程执行不是我们想要的逻辑,我们可以捕获这个termination signal,然后对他做一些自己的处理。注意,windows下不能捕获这些信号,只在*nix下有效。

使用os/signal包提供的封装。

package main

import (
	"fmt"
	"os"
	"os/signal"
	"time"
)
var i int

func init() {
	go SignalHandler(make(chan os.Signal, 1))
}
func main() {
	i = 0
	for {
		i++
		time.Sleep(time.Second)
	}
}

func SignalHandler(c chan os.Signal) {
	signal.Notify(c, os.Interrupt)
	for s:= <-c;; s= <-c {
		switch s {
			case os.Interrupt:
				fmt.Println("interrupt received", i)
				os.Exit(0)  // 去掉这一行,那么Ctrl+c的快捷键将不能中断进程运行
			default:
				fmt.Println("default case")
		}
	}
}

运行上面的代码,在Ctrl-c时可以看到输出 interrupt received 字符串。

除了接收系统的信号,还可以向其他的进行发送信号。

syscall.Kill(syscall.Getpid(), syscall.SIGABRT)

这一部分可能暂时用不上,只要知道有这种用法就可以了。

TCP/IP

使用net提供的TCP/IP封装可以很方便的监听与请求端口。

server(src/wuxu/tcpserver/main.go):

package main

import (
	"net"
	"fmt"
)
var i int
func main() {
	i = 0
	if listener, e := net.Listen("tcp", ":1024"); e == nil {
		for {
			if conn, e := listener.Accept(); e==nil {
				go func(c net.Conn) {
					defer c.Close()
					fmt.Fprintln(c, "hihi", i)
					i++
				}(conn)
			}
		}
	} else {
		fmt.Println("error")
	}
}

client(src/wuxu/tcpclient/main.go)

package main

import (
	"fmt"
	"net"
	"bufio"
)

func main() {
	if conn, e := net.Dial("tcp", "localhost:1024"); e == nil {
		defer conn.Close()
		if text, e := bufio.NewReader(conn).ReadString('\n'); e ==nil {
			fmt.Println(text)
		}
	} else {
		fmt.Println("error", e)
	}
}

加密的TCP通信

还可以很方便地简历加密的连接,为了加密连接我们需要两套密钥对,即服务器的公钥与私钥,客户端的公钥与私钥,使用之前的方法再在服务器和客户端的目录下,各生成一对密钥。go run $GOROOT/src/crypto/tls/generate_cert.go -ca=true -host="localhost"
修改代码如下:

server:

package main

import (
	"fmt"
	"crypto/tls"
	"crypto/rand"
)
var i int
func main() {
	i = 0
	var config tls.Config
	if certificate, e := tls.LoadX509KeyPair("cert.pem", "key.pem"); e ==nil {
		config = tls.Config{
			Certificates: []tls.Certificate{certificate},
			Rand: rand.Reader,
		}
	}
	if listener, e := tls.Listen("tcp", ":1024", &config); e == nil {
		for {
			if conn, e := listener.Accept(); e==nil {
				go func(c *tls.Conn) {
					defer c.Close()
					fmt.Fprintln(c, "secure hihi", i)
					i++
				}(conn.(*tls.Conn))
			}
		}
	} else {
		fmt.Println("error")
	}
}

可以看出,引入tls包后不需要net包了,tls实现了相关得了协议的加密版本。

client:

package main

import (
	"fmt"
	"bufio"
	"crypto/tls"
)

func main() {
	var config tls.Config
	if certificate, e := tls.LoadX509KeyPair("cert.pem", "key.pem"); e == nil {
		config = tls.Config{
			Certificates: []tls.Certificate{certificate},
			InsecureSkipVerify: true,
		}
	}
	if conn, e := tls.Dial("tcp", "localhost:1024", &config); e == nil {
		defer conn.Close()
		if text, e := bufio.NewReader(conn).ReadString('\n'); e ==nil {
			fmt.Println(text)
		}
	} else {
		fmt.Println("error", e)
	}
}

和使用net包的整体流程差不多,使用tls包的实现替换net包。
这样我们就实现了一个使用tls密钥加密的传输通道了。

UDP

UDP的使用和TCP类似也包括一种加密的UDP传输。具体UDP和TCP的区别就不详细说了,这部分是考试面试的重点,一般都能说个123出来。

与TCP实现不一样的是,UDP要先使用net.ResolveUDPAddr(string, string)来获得一个*net.UDPAddr,然后使用net.ListenUDP("udp", addr)监听这个端口的UDP通信。一个简单实现如下:

import (
	. "fmt"
	"net"
	"os"
)

var MSG = ([]byte)("hello golong udp\n")

func main() {
	if addr, e := net.ResolveUDPAddr("udp", ":1026"); e == nil {
		if server, e := net.ListenUDP("udp", addr); e == nil {
			// infinit loop
			for buffer := MakeBuffer(); ; buffer = MakeBuffer() {
				if n, client, e := server.ReadFromUDP(buffer); e == nil {
					go func( c *net.UDPAddr, packet []byte) {
						if n, e := server.WriteToUDP(MSG, c); e == nil {
							Printf("%v bytes written to %v\n", n, c)
						}
					}(client, buffer[:n])
				} else {
					Println("error1", e)
				}
			}
		} else {
			Println("error2", e)
		}
	} else {
		Println("error3", e)
	}
}

func MakeBuffer() ([]byte) {
	return make([]byte, 1024)
}

作为server需要一个无限循环保持监听,每当收到一个客户端请求(ReadFromUDP),就启动一个Goroutine服务这个请求(WriteToUDP)。

UDP不想TCP服务可以用telnet连接,因为它是一个无连接的通信,我们需要自己写一个请求的客户端。client也比较简单,其实现如下:

import (
	. "fmt"
	"net"
	"bufio"
)

var CRLF = ([]byte)("\n")

func main() {
	if addr, e := net.ResolveUDPAddr("udp", "localhost:1026"); e == nil {
		if server, e := net.DialUDP("udp", nil, addr); e == nil {
			defer server.Close()
			// 随意发送一些内容到server等待回应
			// 这里可以多次(for 循环写都可以)写内容到服务器
			if _, e := server.Write(CRLF); e == nil {
				if text, e := bufio.NewReader(server).ReadString('\n'); e == nil {
					Printf("%v", text)
				}
			}
		} else {
			Println("error2", e)
		}
	} else {
		Println("error3", e)
	}
}

这里注意注释里面的话,使用Write()方法向UDP Server发送一些内容,然后才会受到Server的返回,返回使用bufio读取。一个UDP的Dial实例可以多起发送数据和读取返回。

使用加密的UDP

Golang提供了很方便的加密UDP连接,只需要提供一个密钥,就可以对传输的数据进行加密了,当然我们需要对server和client的数据都进行修改。
rsa encrypt server:

import (
	. "fmt"
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha1"
	"encoding/gob"
	. "net"
)

var MSG = []byte("hello go scure udp")
var RSA_LABEL = []byte("server")

func main() {
	Serve(":1025", func(conn *UDPConn, c *UDPAddr, packet *bytes.Buffer) (n int) {
		var key rsa.PublicKey
		if e := gob.NewDecoder(packet).Decode(&key); e==nil {
			if resp, e := rsa.EncryptOAEP(sha1.New(), rand.Reader, &key, MSG, RSA_LABEL); e==nil {
				n, _ = conn.WriteToUDP(resp, c)
			}
		}
		return
	})
}

func Serve(addr string, f func(*UDPConn, *UDPAddr, *bytes.Buffer) int) {
	Launch(addr, func(conn *UDPConn) {
		for {
			buffer := make([]byte, 1024)
			if n, client, e := conn.ReadFromUDP(buffer); e == nil {
				go func(c *UDPAddr, b []byte) {
					if n := f(conn, c, bytes.NewBuffer(b)); n!=0 {
						Println(n, "write to ", c)
					}
				}(client, buffer[:n])
			}
		}
	})
}

func Launch(addr string, f func(*UDPConn)) {
	if a, e := ResolveUDPAddr("udp", addr); e == nil {
		if server, e := ListenUDP("udp", a); e == nil {
			f(server)
		}
	}
}

rsa used client

import (
	. "fmt"
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/x509"
	"encoding/gob"
	"encoding/pem"
	"io/ioutil"
	. "net"
)

var RSA_LABEL = []byte("server")

func main() {
	Connect("localhost:1025", func(server *UDPConn, priKey *rsa.PrivateKey) {
		cipherText := MakeBuffer()
		if n, e := server.Read(cipherText); e == nil {
			Println("cipher text", string(cipherText))
			// cipherText是加密的数据,需要解密
			if plainText, e := rsa.DecryptOAEP(sha1.New(), rand.Reader, priKey, cipherText[:n], RSA_LABEL); e == nil {
				Println("receive decrypt string:", string(plainText))
			}
		}
	})
}

func Connect(address string, f func(*UDPConn, *rsa.PrivateKey)) {
	LoadPrivateKey("key.pem", func(pk *rsa.PrivateKey) { // pk is private key
		if addr, e := ResolveUDPAddr("udp", address); e ==nil {
			if server, e := DialUDP("udp", nil, addr); e == nil {
				defer server.Close()
				SendKey(server, pk.PublicKey, func() {
					f(server, pk)
				})
			} else {
				Println("err1", e)
			}
		} else {
			Println("err2", e)
		}
	})
}

func LoadPrivateKey(file string, f func(*rsa.PrivateKey)) {
	if file, e := ioutil.ReadFile(file); e == nil {
		if block, _ := pem.Decode(file); block != nil {
			if block.Type == "RSA PRIVATE KEY" {
				if key, _ := x509.ParsePKCS1PrivateKey(block.Bytes); key !=nil {
					f(key)
				}
			}
		}
	} else {
		Println(e)
	}
	return
}

func SendKey(server *UDPConn, publicKey rsa.PublicKey, f func()) {
	var encodedKey bytes.Buffer
	if e := gob.NewEncoder(&encodedKey).Encode(publicKey); e == nil {
		if _, e = server.Write(encodedKey.Bytes()); e == nil {
			f()
		}
	}
}

func MakeBuffer() (r []byte) {
	return make([]byte, 1024)
}

需要注意的几点:

  • RSA_LABEL 是rsa加密使用的一个label,需要客户端和服务器端一致
  • 请求流程是客户端读取一个密钥,然后获取一个publickey,使用gob序列化后发送到server, server读取出key,使用这个publicKey对返回的数据进行加密
  • 客户端读取返回后,使用密钥进行解密,读取明文
  • 客户端代码使用了大量的回调,方法字面量,理解起来有点麻烦,可以用javascript的回调的思路去理解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值