=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