=Start=
缘由:
学习、整理一下如何用Go语言进行TCP socket编程,方便以后需要的时候进行参考和使用。
正文:
参考解答:
基于TCP(面向连接)的socket编程,分为客户端和服务器端。
客户端的流程如下:
(1)创建套接字(socket)
(2)向服务器发出连接请求(connect)
(3)和服务器端进行通信(send/recv)
(4)关闭套接字(close)
服务器端的流程如下:
(1)创建套接字(socket)
(2)将套接字绑定到一个本地地址和端口上(bind)
(3)将套接字设为监听模式,准备接收客户端请求(listen)
(4)等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)
(5)用返回的套接字和客户端进行通信(send/recv)
(6)返回,等待另一个客户请求
(7)关闭套接字(close)
从TCP socket诞生后,网络编程架构模型也几经演化,大致是:“每进程一个连接” –> “每线程一个连接” –> “Non-Block + I/O多路复用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴随着模型的演化,服务程序愈加强大,可以支持更多的连接,获得更好的处理性能。
目前主流Web Server一般均采用的都是”Non-Block + I/O多路复用”(有的也结合了多线程、多进程)。不过I/O多路复用也给使用者带来了不小的复杂度,以至于后续出现了许多高性能的I/O多路复用框架, 比如libevent、libev、libuv等,以帮助开发者简化开发复杂性,降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制割裂控制流 的方式依旧复杂,且有悖于“一般逻辑”设计,为此Go语言将该“复杂性”隐藏在Runtime中了:Go开发者无需关注socket是否是 non-block的,也无需亲自注册文件描述符的回调,只需在每个连接对应的goroutine中以“block I/O”的方式对待socket处理即可,这可以说大大降低了开发人员的心智负担。一个典型的Go Server端程序大致如下:
func handleConn(c net.Conn) { defer c.Close() for { // read from the connection // ... ... // write to the connection //... ... } } func main() { l, err := net.Listen("tcp", ":8888") if err != nil { fmt.Println("listen error:", err) return } defer l.Close() for { c, err := l.Accept() if err != nil { fmt.Println("accept error:", err) break } // start a new goroutine to handle // the new connection. go handleConn(c) } }
上面的内容可能更偏概念性的描述,下面通过几个简单的例子帮助建立起直观的认识。
客户端代码:
package main import ( "bufio" "fmt" "net" "os" "strconv" ) const ( SERVER_IP = "127.0.0.1" SERVER_PORT = 10006 SERVER_RECV_LEN = 10 ) func checkError(err error) { if err != nil { fmt.Println(err) os.Exit(1) } } func main() { address := SERVER_IP + ":" + strconv.Itoa(SERVER_PORT) fmt.Printf("value of address: %v (%T)\n\n", address, address) conn, err := net.Dial("tcp", address) checkError(err) defer conn.Close() input := bufio.NewScanner(os.Stdin) for input.Scan() { line := input.Text() lineLen := len(line) n := 0 for written := 0; written < lineLen; written += n { var toWrite string if lineLen-written > SERVER_RECV_LEN { toWrite = line[written : written+SERVER_RECV_LEN] } else { toWrite = line[written:] } n, err = conn.Write([]byte(toWrite)) checkError(err) fmt.Println("Write:", toWrite) msg := make([]byte, SERVER_RECV_LEN) n, err = conn.Read(msg) checkError(err) fmt.Println("Response:", string(msg)) } } }
服务端(单进程/单线程/单goroutine)代码:
package main import ( "fmt" "net" "os" "strconv" "strings" ) const ( SERVER_IP = "127.0.0.1" SERVER_PORT = 10006 SERVER_RECV_LEN = 10 ) func main() { address := SERVER_IP + ":" + strconv.Itoa(SERVER_PORT) listener, err := net.Listen("tcp", address) if err != nil { fmt.Println(err) os.Exit(1) } defer listener.Close() for { conn, err := listener.Accept() if err != nil { fmt.Println(err) continue } defer conn.Close() fmt.Println("Client Info:", conn.RemoteAddr().String()) for { data := make([]byte, SERVER_RECV_LEN) _, err = conn.Read(data) if err != nil { fmt.Println(err) break } strData := string(data) fmt.Println("Received:", strData) upper := strings.ToUpper(strData) _, err = conn.Write([]byte(upper)) if err != nil { fmt.Println(err) break } fmt.Println("Send:", upper) } } }
上面的服务端程序存在一个问题,那就是一次只能为一个客户端提供服务(for循环),如果一个客户端连接时间过长,可能会让服务器无法服务于其他的客户端,这就是拒绝服务攻击,最简单的解决这种问题的方式就是将conn的处理单独启动线程,在Go 语言中我们只需要借助于 go 方法启动一个routine即可。
package main import ( "fmt" "net" "os" "strconv" "strings" ) const ( SERVER_IP = "127.0.0.1" SERVER_PORT = 10006 SERVER_RECV_LEN = 10 ) func main() { address := SERVER_IP + ":" + strconv.Itoa(SERVER_PORT) listener, err := net.Listen("tcp", address) if err != nil { fmt.Println(err) os.Exit(1) } defer listener.Close() for { conn, err := listener.Accept() if err != nil { fmt.Println(err) continue } go handleConn(conn) } } func handleConn(conn net.Conn) { defer conn.Close() fmt.Println("Client Info:", conn.RemoteAddr().String()) for { data := make([]byte, SERVER_RECV_LEN) _, err := conn.Read(data) if err != nil { fmt.Println(err) break } strData := string(data) fmt.Println("Received:", strData) upper := strings.ToUpper(strData) _, err = conn.Write([]byte(upper)) if err != nil { fmt.Println(err) break } fmt.Println("Send:", upper) } }
将客户端数据处理的部分封装成了一个handleConn函数,在与客户端连接建立时,通过Go 语言的go关键字调用handleConn函数,启动一个新的协程进行处理,这样,main函数会立刻重新阻塞在Accept上,从而保证服务器可以时刻监听客户端连接的建立,提高了处理性能。
参考链接:
- golang TCP Socket编程
- 利用golang通道优化TCP Socket服务器
- Golang Socket编程
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.1.md - Go语言TCP Socket编程
https://github.com/bigwhite/experiments/tree/master/go-tcpsock - golang之TCP开发
- https://golang.org/pkg/net/#example_Listener
https://golang.org/pkg/net/#TCPAddr
https://golang.org/pkg/net/#Dial
https://golang.org/pkg/net/#Listen
=END=
《 “Go语言学习#17-如何进行TCP socket编程” 》 有 3 条评论
golang使用UDP进行网络通信
https://blog.csdn.net/yjp19871013/article/details/82316299
一个简单的Golang实现的Socket5 Proxy
http://www.flysnow.org/2016/12/26/golang-socket5-proxy.html
Go语言的变量、函数、Socks5代理服务器
https://blog.csdn.net/ithomer/article/details/78121431
Provides packet processing capabilities for Go
https://github.com/google/gopacket
Packet Capture, Injection, and Analysis with Gopacket
https://www.devdungeon.com/content/packet-capture-injection-and-analysis-gopacket
Go语言用GoPacket抓包分析
https://blog.csdn.net/u014762921/java/article/details/78275428
Golang调用gopacket库编写网络数据包捕获、注入、分析工具
https://blog.lfoder.cn/2018/06/17/Golang%E8%B0%83%E7%94%A8gopacket%E5%BA%93%E7%BC%96%E5%86%99%E7%BD%91%E7%BB%9C%E6%95%B0%E6%8D%AE%E5%8C%85%E6%8D%95%E8%8E%B7%E3%80%81%E6%B3%A8%E5%85%A5%E3%80%81%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/
Capture and parse http traffics
https://github.com/hsiafan/httpdump
Sniffing and parsing mysql,redis,http,mongodb etc protocol. 抓包截取项目中的数据库请求并解析成相应的语句。
https://github.com/40t/go-sniffer