GoWeb——Socket编程

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
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值