go socket
socket编程,即网络编程,自从tcp-socket诞生后,网络编程的架构也进行了几次变化。从一开始的 “一个进程”→“一个连接”,变成“一个线程”→“一个连接”,到现在主流的“Non-Block + I/O多路复用”。经过这几次的变化,应用程序拥有更好的处理性能,可以同时支持更多的连接,但是I/O多路复用给使用者带来了不小的复杂度,在golang里面,设计者将这些复杂性隐藏在 runtime 中,开发者只需在每个连接对应的 goroutine 中以“block I/O”的方式对待 socket 处理即可。说到底 socket 也只是操作系统抽象出来的,用来方便我们使用通信协议进行多主机间通讯的一种方式,下面分别介绍 tcp-socket,udp-socket,http-request、response的基本编程方法,首先是net包的相关方法:
方法 | 作用 |
---|---|
ResolveTCPAddr(network string,address string) | 解析IP地址和port,返回TCPAddr类型的指针,network=网络类型,例:tcp(默认)/tcp4/tcp6/udp/ip address=服务器IP/域名:端口 |
ResolveUDPAddr(network string,address string) | 作用、参数同上,返回UDPAddr类型的指针 |
ResolveIPAddr(network string,address string) | 作用、参数同上,返回IPAddr类型的指针 |
Listen(network string,address string) | 开启监听,手动设置监听的协议network=网络类型,例:tcp(默认)/tcp4/tcp6/udp/ip address=本机需要监听的 ip 和 port,如果为 nil 或者未指定 ip,则监听广播 |
ListenTCP(network string,address string) | 开启监听TCP连接,返回一个*TCPlistener类型数据 |
ListenUDP(network string,address string) | 开启监听UDP连接,返回一个*UDPConn类型数据 |
ListenIP(network string,address string) | 开启监听IP连接,返回一个*IPConn类型数据 |
Dial(network string,address string) | 客户端主动连接服务器,是对Dial系列方法的封装,network=网络类型 address=服务器地址(ip/域名:port) |
DialTimeout(network string,address string,timeout time.Duration) | 在主动连接的基础上设置了超时时间,network、address同上,timeout=自定义超时时间(例:3*time.second) |
DialTCP(network string, laddr *TCPAddr, raddr *TCPAddr) | 建立 TCP 连接,network=网络类型 laddr=本地地址,通常为nil raddr=目的地址(类型为TCPAddr的指针) |
DialUDP(network string, laddr *UDPAddr, raddr *UDPAddr) | 建立 UDP连接,参数同上 |
DialIP(network string, laddr *IPAddr, raddr *IPAddr) | 建立ICMP连接,参数同上 |
net.Listen()系列【数据接收】:
方法 | 作用 |
---|---|
Accept() | 接收发送端的数据(不限定类型) |
AcceptTCP() | 只接收TCP类型的发送端数据 |
Close() | 停止监听 |
net.Listen().Accept()系列【TCP数据处理】:
方法 | 作用 |
---|---|
Read(b []byte) | 实现 Read 接口,把接收到的信息放在切片里读取 |
ReadFrom(r Reader) | 和Read()相似,但是从对象中读取(例:文件对象) |
Write(b []byte) | 实现 Write 接口,把要发送的信息通过切片写入 |
Close() | 关闭连接 |
net.Listen().Read()系列【UDP数据处理】:
方法 | 作用 |
---|---|
Read(b []byte) | 同TCP数据处理的Read(),用于从字节流读取数据 |
ReadFrom(r Reader) | 同TCP数据处理的ReadFrom(),用于从对象中读取数据 |
ReadFromUDP(b []byte) | 获取一个完整的UDP数据包,从中读取信息;若缓冲区设置过小,不足以容纳一个UDP包,会引起报错 |
ReadMsgUDP(b []byte,oob []byte) | 暂时不知道用处 |
Write(b []byte) | 同TCP数据处理的Write(),用于发送数据字节流 |
客户端连接建立后,返回一个 net.Conn 接口类型变量值,后续通过连接返回的变量值调用的 Write、Read 和 Close 方法的作用同服务端。
TCP
众所周知,tcp 建立连接要进行三次握手,因此 go-socket 也有对应的信息发送端(客户端)及接收端(服务端);
go-socketTCP 通信过程:
- 服务器开启监听
- 客户端与服务器建立连接
- 客户端发送信息
- 服务端接收到信息并处理
如图,每一个 socket 通信的步骤都有对应的方法实现,在这些底层方法上又有一个新的方法将它们封装,例如 Listen(),这个函数里面同时封装了监听 TCP 端口的方法 ListenTCP() 和监听 UDP 端口的方法 ListenUDP(),再往上则有一个 DialTCP() 函数将服务端和客户端除了读写和关闭连接外的所有功能都封装起来,也就是说 DialTCP() 是不分客户端和服务端的,两者通用。
下面是简单的例子:
客户端
客户端用 net.Dail 或 DialTimeout 建立连接,对应信息的发送方。
package sockApp
import (
"log"
"net"
)
func Connecter() () {
//客户端主动连接,使用方法net.Dial()
conn,err:=net.Dial("tcp4","127.0.0.1:8888")
if err != nil {
log.Printf("连接报错:%v",err)
return
}
defer conn.Close() //当函数调用结束时关闭连接
conn.Write([]byte("Hello")) //发送信息
}
服务端
服务端是标准的 listen+accept 结构,对应信息接收方(设置监听)
package sockApp
import (
"log"
"net"
)
func Listener() {
//解析本地地址并开启监听
addr,_:=net.ResolveTCPAddr("tcp4","127.0.0.1:8888")
lis,_:=net.ListenTCP("tcp4",addr)
log.Println("开始监听...")
for {
accept,_:=lis.Accept() //接收数据
//处理数据
go func(){
getByte:=make([]byte,1024) //创建1024缓冲区的切片用于数据处理
n,_:=accept.Read(getByte)
log.Println(string(getByte[:n])) //打印数据
accept.Close() //关闭连接
}()
}
}
UDP
UDP 通信就没有那么多讲究了,客户端和服务端之间并不会建立三次握手,而是客户端直接向服务端发送数据,但是在 go-socket 层面编程的步骤和tcp一样;
go-socketUDP 通信过程:
- 服务器开启监听端口
- 客户端发送信息
- 服务端接收信息并处理
和 TCP 端一样,这里也是每一个步骤对应一个底层方法,上面也有函数对这些方法进行封装。由于 UDP 通信面向无连接,因此多出一个 WriteToUDP() 方法,可以直接将数据发送到目标主机,省去了创建套接字的过程。DialUDP() 的作用和 DialTCP() 一样,可以用作客户端也可以用作服务端的套接字创建和监听端口。
下面是简单的例子:
客户端
客户端代码和TCP类似,用 Dial() 方法创建好套接字,然后直接调用 Write() 发送。
package sockApp
import (
"log"
"net"
)
func Connecter() {
//客户端主动通信,使用方法net.Dial()
conn,err:=net.Dial("udp4","127.0.0.1:8888")
if err != nil {
log.Printf("连接报错:%v",err)
return
}
defer conn.Close() //当函数调用结束时关闭连接
conn.Write([]byte("Hello")) //发送信息
log.Println("信息发送完成...")
}
服务端
因为连接的不可靠性,UDP socket 并没有像 TCP 那样的 Accept() 方法,但其他部分和 TCP 几乎一模一样,因为这些函数可能同时封装了针对这两种协议的底层方法。
package sockApp
import (
"log"
"net"
)
func Listener() {
//服务端监听端口
log.Println("Socket Begin...")
addr,_:=net.ResolveUDPAddr("udp4","127.0.0.1:8888")
lis,_:=net.ListenUDP("udp4",addr)
log.Println("开始监听...")
for {
//处理数据
go func(){
b:=make([]byte,1024) //创建1024缓冲区的切片用于接收数据
n,_,err:=lis.ReadFromUDP(b) //接收数据
if err != nil {
log.Printf("数据接收报错:%v",err)
return
}
log.Println(string(b[:n])) //打印数据
}()
}
}
多线程应用&常见问题
Golang 是一种以高效的并发处理能力著称的语言,在程序运行的时候可以通过启动一个新的 goroutine 来处理相应任务,其返回结果放进通道进行传输。
如图,这是一个 socket 通信的实际应用场景,客户端 A 和 B 所在的两个内网是相互隔绝的,边界防火墙的 NAT 网络保护着他们同时也在节省 IP 地址。这种网络有个特点,就是外部请求内网主机的数据包在经过 NAT 网关的时候会因为没有保存过该地址而直接丢掉,并留下记录。相反,内网主机可以通过 NAT 网关主动与外网服务器建立 session 实现通信,这个 session 是临时的,只会存在几分钟到几小时不等,而且是定向的,只能是从 NAT 网关的某个端口到目标服务器的某个端口。那么现在想要穿过 NAT 网关的封锁访问内网主机 B,就需要两个条件:
- 主机 B 知道主机 A 的存在并且认为数据包可以到达;
- 主机 B 主动与外界建立连接;
于是就有了上图所示的内网穿透技术,首先客户端 A、B 主动向中转服务器发起连接,建立临时 session,然后由服务器向被访问方转发客户端 A 的探测包(源地址=A 的外网地址,目的地址=B 的外网地址),这个包会被客户端 B 所在内网的 NAT 网关丢掉,但是没关系,客户端 B 已经知道 A 是可到达的了,下一步客户端 B 就可以用这条记录上的地址作为目的地址发起访问,从而实现 客户端 A —— 客户端 B 的通信。这一步有个要注意的地方,如果在客户端 B 连接 客户端 A 的时候,A 的端口已经重新分配了,那么也是无法通信的。
下面实例将实现一个 P2P 连接,两端利用 UDP 协议通过一个中转服务器进行通信,实现内网穿透,并且在中转服务器和客户端上用多线程的技术处理信息传递和接收,达到提升效率的效果:
客户端代码:
package main
import (
"fmt"
"log"
"net"
"strconv"
"strings"
"time"
)
//发送心跳包,主动与外网中转服务器通信;
func heartbeat(local_sock *net.UDPAddr,server string) (connection *net.UDPConn){
server_sock := &net.UDPAddr{
IP: net.ParseIP(server),Port: 8888}
conn,err_connect := net.DialUDP("udp4",local_sock,server_sock)
if err_connect != nil{
fmt.Printf("连接创建出错:%s\n",err_connect)}
_,err_write