1、Go语言中的Socket
在Go语言中进行网络编程,比传统的网络编程更加简洁。Go语言提供了net包来处理Socket。net包对Socket连接过程进行了抽象和封装,无论使用什么协议建立什么形式的连接,都只需要调用net.Dial()函数即可,从而大大简化了代码的编写量。
在服务器端和客户端的通信过程中,服务器端有两个Socket连接参与进来,但用于通信的只有conn结构体中的Socket连接。conn是由listener创建的用于Socket连接的结构体,隶属于服务器端。
服务器端通过net.Listen()方法建立连接并监听指定IP地址和端口号,等待客户端连接。客户端则通过net.Dial()函数连接指定的IP地址和端口号,建立连接后即可发送消息,如图所示。
2、客户端Dial()函数
2.1、Dial()函数的定义
在Go语言中,net包,的Dial()函数的定义如下:
func Dial(net,addr string)(Conn,error)
其中,net参数是网络协议的名字,addr参数是IP地址或域名,而端口号以“:”的形式跟随在P地址或域名的后面,端口号可选。如果连接成功,则返回连接对象,否则返回error。
2.2、Dial()函数的使用
2.2.1、 TCP连接
TCP连接直接通过net.Dial(“tcp”,“ip:port”)的形式调用:
conn,err := net.Dia1("tcp","192.168.0.1:8087")
2.2.2、UDP连接
UDP连接直接通过net.Dial(“udp”,“ip:port”)的形式调用:
conn,err := net.Dia1("udp","192.168.0.2:8088")
2.2.3、ICMP连接(使用协议名称)
ICMP连接(使用协议名称)通过net.Dial(“ip4:icmp”,“www.shirdon.com”)的形式调用:
conn,err := net.Dial("ip4:icmp","www.shirdon.com"
2.2.4、ICMP连接(使用协议编号)
ICMP连接(使用协议名称)的用法如下:
conn,err := net.Dia1("ip4:1","10.0.0.8")
目前,Dial()函数支持如下几种网络协议:TCP、TCP4(仅限IPv4)、TCP6(仅限IPv6)、UDP、UDP4(仅限IPv4)、UDP6(仅限IPv6)、IP、IP4(仅限IPv4)和IP6(仅限IPv6)。
在成功建立连接后,就可以进行数据的发送和接收。使用Write()方法发送数据,使用Read()方法接收数据。下面这个示例代码展示了使用Read()方法来接收数据。
import (
"bytes"
"fmt"
"io"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
validateError(err)
//调用由返回的连接对象提供的Write()方法发送请求
_, err = conn.Write([]byte("hello world"))
validateError(err)
result, err := fullyRead(conn)
validateError(err)
fmt.Println(string(result))
os.Exit(0)
}
// 如果连接出错,则打印错误消息并退出程序
func validateError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
// 通过由连接对象提供的Read()方法读取所有响应数据
func fullyRead(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
_, err := conn.Read(buf[0:])
result.Write(buf[0:])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
}
return result.Bytes(), nil
}
3、客户端DialTCP()函数的使用
3.1、DialTCP()函数的定义
除Dial()函数外,还有一个名为DialTCP()的函数用来建立TCP连接。
DialTCP()函数和Dial()函数类似,该函数的定义如下:
func DialTCP(network string, laddr, raddr *TCPAddr)(*TCPConn, error)
其中,network参数可以是tcp、tcp4或tcp6;laddr为本地地址,通常为nil;raddr为目的地址,为TCPAddr类型的指针。该函数返回一个*TCPConn对象,可通过Read()和Write()方法传递数据。例如要访问网址127.0.0.1:8086,则使用方法见下方示例。
import (
"fmt"
"io"
"net"
"os"
)
func main() {
service := "127.0.0.1:8888"
tcpAddr, err := net.ResolveTCPAddr("tcp", service)
checkError(err)
fmt.Println("tcpAddr:")
typeof(tcpAddr)
myConn, err1 := net.DialTCP("tcp", nil, tcpAddr)
checkError(err1)
fmt.Println("myConn:")
typeof(myConn)
_, err = myConn.Write([]byte("hello world!"))
checkError(err)
result, err := io.ReadAll(myConn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
tcpAddr:
type is:*net.TCPAddr
myConn:
type is:*net.TCPConn
3.2、DialTCP()函数的使用
3.2.1、TCP服务器端代码
编写一个TCP服务器端程序,在8088端口监听;可以和多个客户端建立连接;连接成功后,客户端可以发送数据,服务器端接收数据,并显示在命令行终端。先使用telnet来测试,然后编写客户端程序来测试。该程序的服务器端和客户端的示意图如图所示。
import (
"fmt"
"log"
"net"
)
func main() {
Server()
}
func Server() {
//用Listen()函数创建的服务器端
//tcp: 网络协议
//本机的IP地址和端口号:127.0.0.1:8088
l, err := net.Listen("tcp", "127.0.0.1:8088")
if err != nil {
log.Fatalln(err)
}
defer l.Close()
//循环等待客户端范文
for {
conn, err := l.Accept()
if err != nil {
log.Fatalln(err)
}
fmt.Printf("范文客户端信息:con=%v客户端 ip=%v\n", conn, conn.RemoteAddr().String())
go handleConnection(conn)
}
}
//服务端处理从客户端接收的数据
func handleConnection(c net.Conn) {
defer c.Close()
for {
//1. 等待客户端捅咕oconn对象发送信息
//2. 如果客户端没有发送数据,则goroutine就阻塞在这里
fmt.Printf("服务器在等待客户端%s发送信息\n", c.RemoteAddr().String())
buf := make([]byte, 1024)
n, err := c.Read(buf)
if err != nil {
log.Fatalln(err)
break
}
//3. 显示客户端发送到服务器端的内容
fmt.Println(string(buf[:n]))
}
}
3.2.2、TCP客户端代码
编写一个TCP客户端程序,该客户端有如下的功能:
- 能连接到服务器端的8088端口;
- 客户端可以发送单行数据,然后退出;
- 能通过客户端命令行终端输入数据(输入一行就发送一行),并发送给服务器端;
- 在客户端命令行终端输入exit,表示退出程序。
import (
"bufio"
"fmt"
"log"
"net"
"os"
"strings"
)
func main() {
Client()
}
func Client() {
conn, err := net.Dial("tcp", "127.0.0.1:8088")
if err != nil {
log.Fatalln(err)
}
//客户端可以发送单行数据,然后退出
reader := bufio.NewReader(os.Stdin)
for {
//从客户端读取一行用户输入,并准备发送给服务器端
line, err := reader.ReadString('\n')
if err != nil {
log.Fatalln(err)
}
line = strings.Trim(line, "\r\n")
if line == "exit" {
fmt.Println("用户退出客户端")
break
}
//将line发送给服务器端
conent, err := conn.Write([]byte(line + "\n"))
if err != nil {
log.Fatalln(err)
}
fmt.Printf("客户端发送了%d字节的数据到服务器端\n", conent)
}
}
4、UDP Socket的使用
4.1、UDP Socket的定义
由于UDP是“无连接”的,所以服务器端只需要指定P地址和端口号,然后监听该地址,等待客户端与之建立连接,两端即可通信。
下面在Go语言中创建UDP Socket,用函数或者方法来实现。
4.1.1、创建监听地址
创建监听地址使用ResolveUDPAddr()函数,其定义如下:
func ResolveUDPAddr(network,address string)(*UDPAddr,error)
4.1.2、创建监听连接
创建监听连接使用ListenUDP()函数,其定义如下:
func ListenUDP(network string,laddr UDPAddr)
(UDPConn,error)
4.1.3、接收UDP数据
接收UDP数据使用ReadFromUDP()方法,其定义如下:
func (c *UDPConn)ReadFromUDP(b []byte)(int, *UDPAddr,error)
4.1.4、写出数据到UDP
写出数据到UDP使用WriteToUDP()方法,其定义如下:
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
4.2、UDP Socket的使用
4.2.1、UDP服务器端代码
import (
"fmt"
"net"
)
func main() {
//创建监听的地址,并且指定为UDP协议
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8012")
if err != nil {
fmt.Println("ResolveUDPAddr err:", err)
return
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
fmt.Println("ListenUDP err", udpAddr)
return
}
defer conn.Close()
buf := make([]byte, 1024)
//接收客户端发送过来的数据,并填充到切片buf中
n, raddr, err := conn.ReadFromUDP(buf)
if err != nil {
return
}
fmt.Println("客户端发送:", string(buf[:n]))
_, err = conn.WriteToUDP([]byte("您好,客户端,我是服务器端"), raddr) //想客户端发送数据
if err != nil {
fmt.Println("WriteToUDP err:", err)
return
}
}
4.2.2、UDP客户端代码
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("udp", "127.0.0.1:8012")
if err != nil {
fmt.Println("net.Dial err:", err)
return
}
defer conn.Close()
conn.Write([]byte("你好,我是用UDP的客户端"))
buf := make([]byte, 1024)
n, err1 := conn.Read(buf)
if err1 != nil {
return
}
fmt.Println("服务器发来:", string(buf[:n]))
}
4.2.3、UDP并发编程
要实现UDP并发编程,需要在UDP客户端通过go关键字启动goroutine来处理请求。同时在服务器端需要通过for语句循环处理客户端数据。
1. 并发版UDP服务器端:
import (
"fmt"
"net"
)
func main() {
//创建服务器端UDP地址结构:指定IP地址和端口号
laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8023")
if err != nil {
fmt.Println("ResolveUDPAddr err:", err)
return
}
//监听客户端连接
conn, err := net.ListenUDP("udp", laddr)
if err != nil {
fmt.Println("net.ListenUDP err:", err)
return
}
defer conn.Close()
for {
buf := make([]byte, 1024)
n, raddr, err := conn.ReadFromUDP(buf)
if err != nil {
fmt.Println("conn.ReadFromUDP err:", err)
return
}
fmt.Printf("接收到客户端[%s]:%s", raddr, string(buf[:n]))
conn.WriteToUDP([]byte("i am server"), raddr)
}
}
2. 并发版UDP客户端:
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("udp", "127.0.0.1:8023")
if err != nil {
fmt.Println("net.Dial err:", err)
return
}
defer conn.Close()
//通过go关键字启动goroutine,从而支持并发
go func() {
var str string
for {
_, err := fmt.Scanln(&str)
if err != nil {
fmt.Println("os.Stdin.err = ", err)
return
}
conn.Write([]byte(str))
}
}()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
fmt.Println("服务器发送来:", string(buf[:n]))
}
}
5、简单的聊天程序
5.1、服务器端代码编写
import (
"fmt"
"net"
"time"
)
var ConnSlice map[net.Conn]*Heartbeat
// 心跳结构体
type Heartbeat struct {
endTime int64 //过期时间
}
func main() {
ConnSlice = map[net.Conn]*Heartbeat{}
l, err := net.Listen("tcp", "127.0.0.1:8086")
if err != nil {
fmt.Println("服务器启动失败")
}
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err)
}
fmt.Printf("Received message %s -> %s \n", conn.RemoteAddr(), conn.LocalAddr())
ConnSlice[conn] = &Heartbeat{
endTime: time.Now().Add(time.Second * 5).Unix(), //初始化过期时间
}
go handelConn(conn)
}
}
func handelConn(c net.Conn) {
buffer := make([]byte, 1024)
for {
n, err := c.Read(buffer)
if ConnSlice[c].endTime > time.Now().Unix() {
ConnSlice[c].endTime = time.Now().Add(time.Second * 5).Unix() //更新心跳时间
} else {
fmt.Println("长时间未发消息断开连接")
return
}
if err != nil {
return
}
//如果是心跳检测,那就不要执行剩下的代码
if string(buffer[0:n]) == "1" {
c.Write([]byte("1"))
continue
}
for conn, heart := range ConnSlice {
if conn == c {
continue
}
//心跳检测 使用懒惰更新,需要发送数据的时候才检查规定时间内有没有数据到达
if heart.endTime < time.Now().Unix() {
delete(ConnSlice, conn) //从房间列表中删除连接,并且关闭
conn.Close()
fmt.Println("删除连接", conn.RemoteAddr())
fmt.Println("现在存有链接", ConnSlice)
continue
}
conn.Write(buffer[0:n])
}
}
}
5.2、客户端代码编写
import (
"bufio"
"fmt"
"net"
"os"
"time"
)
func main() {
server := "127.0.0.1:8086"
tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
if err != nil {
Log(os.Stderr, "Fatal error:", err.Error())
os.Exit(1)
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
Log("Fatal error:", err.Error())
os.Exit(1)
}
Log(conn.RemoteAddr().String(), "connect success!")
Sender(conn)
Log("end")
}
func Sender(conn *net.TCPConn) {
defer conn.Close()
sc := bufio.NewReader(os.Stdin)
go func() {
t := time.NewTicker(time.Second) //创建定时器,用来实现定期发送心跳包给服务端
defer t.Stop()
for {
<-t.C
_, err := conn.Write([]byte("1"))
if err != nil {
fmt.Println(err.Error())
return
}
}
}()
name := ""
fmt.Println("请输入聊天昵称") //用户聊天的昵称
fmt.Fscan(sc, &name)
msg := ""
buffer := make([]byte, 1024)
_t := time.NewTimer(time.Second * 5) //创建定时器,每次服务端发送消息就刷新时间
defer _t.Stop()
go func() {
<-_t.C
fmt.Println("服务器出现故障,断开链接")
return
}()
for {
go func() {
for {
n, err := conn.Read(buffer)
if err != nil {
return
}
_t.Reset(time.Second * 5) //收到消息就刷新_t定时器,如果time.Second*5时间到了,那么就会<-_t.C就不会阻塞,代码会往下走,return结束
if string(buffer[0:1]) != "1" { //心跳包消息定义为字符串"1",不需要打印出来
fmt.Println(string(buffer[0:n]))
}
}
}()
fmt.Fscan(sc, &msg)
i := time.Now().Format("2006-01-02 15:04:05")
conn.Write([]byte(fmt.Sprintf("%s\n\t%s: %s", i, name, msg))) //发送消息
}
}
func Log(v ...interface{}) {
fmt.Println(v)
return
}