golang网络编程day2
- golang socket编程
- golang TCP编程
- golang TCP流行框架
- golang 游戏服务器框架
- golang udp编程
今天的学习主要注重实践,有了昨天的理论基础,今天的目的就是看例子懂例子,写例子,知道学的东西在例子里面是怎么发挥作用的。
golang socket编程
首先知道socket编程主要是用于实现不同主机之间的数据交换和通信,socket提供了一种方式来建立网络连接使得不同设备上的程序能够收发数据。
socket的应用场景
1.实现客户端和服务器之间的通信:比如web服务器和浏览器交互
2.构建分布式应用:允许不同位置的应用程序组件相互通信
3.网络服务的创建:电子邮件,FTP,HTTP服务等
4.实现即使通信工具:如聊天应用,实时数据传输等。
下面直接看一个socket编程的小例子
把这个例子理解好,自己能写一个就是懂了。
package main
import (
"fmt"
"net"
)
func main() {
// 建立一个TCP连接,连接到远程主机的80端口
conn, err := net.Dial("tcp", "example.com:80")
if err != nil {
fmt.Println("Error connecting:", err)
return
}
// 发送一个HTTP GET请求
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
// 从连接中读取服务器的响应
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if n == 0 || err != nil {
break
}
fmt.Print(string(buf[0:n]))
}
}
我拿到这个例子的时候我当时是没看出来用了socket编程,只知道用了个net包来实现。接下来具体解析。
首先这个几个函数干啥用的必须了解清楚
net.Dial(“tcp”, “example.com:80”)
解读:两个参数,第一个是网络类型(这里是tcp)和地址(这个是一个服务器地址并且带端口号),这个端口号我要说明一下,这个端口号不是乱取的,80端是HTTP服务通常监听的端口。返回值是net.Conn和err,net.Conn是go语言标准库中net包里面的一个接口,它代表了一个网络连接,该接口封装了网络通信的细节,提供了一系列用于数据交换的方法。
net.Conn接口具体:
type Conn interface {
//从连接中读取数据,并把数据放入指定的字节切片b中,返回值n是读了多少数据
Read(b []byte) (n int, err error)
//向连接中写数据,数据存在b里面然后写入连接,返回值n是写了多少数据
Write(b []byte) (n int, err error)
//关闭连接,注意调用之后,任何阻塞的Read或Write操作都将接触阻塞并返回错误
Close() error
//返回连接的本地端点地址
LocalAddr() Addr
//返回连接的远程端点的位置
RemoteAddr() Addr
//设置读写操作的截止时间,超过这个时间,读写操作将被取消并返回超时错误
SetDeadline(t time.Time) error
//设置读取操作~
SetReadDeadline(t time.Time) error
//设置写入操作~
SetWriteDeadline(t time.Time) error
}
这上面比较难理解的就是这两个方法
LocalAddr()Addr
返回连接的本地端点地址:指的就是发起网络连接的设备(例如我的电脑)的网络地址。这通常包括一个IP地址和一个端口号。例如,如果我的电脑上运行的程序连接到了一个服务器,那么该程序的IP地址和用于建立连接的端口号就构成了这个所谓的本地端点地址。
RemoteAddr() Addr
返回远程端点地址:指的是网络连接另一端的设备。比如我正在连接的服务器的IP地址。同样的它包括IP地址和端口号。比如我的程序连接到了一个web服务器,那么该服务器的IP地址和它监听的端口就构成了这个所谓的远程端点地址。
Addr类型:
Addr是go语言net包中的一个接口,它用于表示网络地址。这个接口定义了几个非常常用的方法,主要用于获取地址的字符串表示和进行比较。
这个接口的具体定义如下:
type Addr interface {
Network() string // 返回网络类型
String() string // 返回网络地址的字符串表示形式
}
对于udp和tcp网络,这个字符串表示是IP:端口这样的形式。
比如我在调用LocalAddr()和RemoteAddr()这两个方法时,我就可以进一步调用里面的String()方法直接获取网络地址的详细信息。这种要求显然是非常的常用的。
懂了这些知识,这里我就总结这个net.Conn有啥用:
net.Conn是一个网络连接,我拿到了这个连接,那我就可以进行网络数据的读写,还可以进行网络声明周期的管理,还可以获取到这个连接的相关信息。这些功能全是通过这个接口的方法来实现的。也就是我拿到net.Conn我就可以开始操作了。
fmt.Fprintf(conn, “GET / HTTP/1.0\r\n\r\n”)
这个函数是用于向这个conn这个数据连接里面写入数据。这个例子我是发送了HTTP GET请求,这个里面的实参是HTTP协议标准的请求格式,包括请求行和请求头的结束。
conn.Read(buf)
这个刚刚也介绍过了,这个就是conn这个类型的一个方法,用于从这个连接里面读取数据,也就是接受服务器的响应,这个数据会存储到buf这个字节切片里面。这个函数的返回值我之前也介绍过了。
这里是我在看这个例子里面的一些思考,因为我还是不懂的太多了。
个人思考
1.这个例子叫socket编程,为什么我感受不到用了socket?
解答:首先这个例子做了一个事,建立tcp连接然后发送了一个HTTP请求。
为什么体会不到用了Socket。这个例子中,可以发现就调用了一个net.Dial()函数就建立了连接。哪里又socket,实际上Dial是封装了底层socket调用的高级函数,用于创建一个客户端socket并连接到指定的服务器。这个过程中,socket的创建和连接管理都被net包内部处理了,所以才看不见传统意义上的socket编程,像创建socket、绑定地址、监听端口号这些操作。这也是go语言的一大优势,简洁强大,隐藏了复杂性。
2.为啥能发HTTP请求,因为http请求的传输就是TCP。
3.我自己写的时候发现fmt.Print(string(buf))和fmt.Print(string(buf[0:n]))有差别,会输出一堆红色的null,这是因为它会输出整个字节切片,包括未使用部分可能存在的空字符串\x00,用n就限定了只输出实际用到的部分。
4.为什么使用字节切片,因为用字节切片存的数据都是ASCII码,这样处理会更加的高效和快速。
了解完这些我相信这个代码例子已经可以看懂了。
总结:用net包里面的Dial()拨号函数,直接建立起了到目标IP地址服务器的连接,得到这个连接后我可以进行相关的操作,比如我就调了一个Fprintf函数往这个连接里面发了个HTTP Get请求。由于HTTP是请求和响应这样的工作方式,我就掉了这个Conn的Read,读取这个HTTP的响应。最后把读出的内容打印出来。
除了上面例子里面用到的函数,还有很多的函数是很常用的网络编程函数,比如net.Listen,net.Accept,net.DialTimeout,net…ResolveTCPAdddr等等。后续进行介绍。
TCP编程
go语言中的TCP通信主要靠net包来实现
下面是一些常用的与tcp相关的api。目标是搞懂这些函数有什么用和一些相关的细节,然后自己能写出这个案例。
1.net.Listen(“tcp”,address string)(Listener,error) :创建一个TCP服务器,并且监听指定的地址。
详细解读:
type Listener interface {
//接受传入的连接
Accept() (Conn, error)
//关闭监听器
Close() error
//返回监听器的网络地址
Addr() Addr
}
这是返回值的细节,它的这个Listener返回值是一个接口类型,可以理解为返回了一个监听器,用于接收传入的连接(这是主要功能)。
2.Listener.Accept()(Conn,error):用于接受一个客户端连接请求,返回一个Conn对象(也就是一个网络连接,这样我就可以进行相关操作了,传读数据等等),在服务器中通常会使用一个无限循环来接受多个客户端连接请求。
比如:
for {
conn, err := listener.Accept()
if err != nil {
// 处理错误
continue
}
// 处理连接
}
显然这个函数有阻塞功能,它会等待并返回一个新连接或发生错误,这就是为什么他通常与无限循环一起使用,以实现持续接受传入的连接,当没有连接时会一直阻塞等待直到有新的连接到来。
3.Conn.Read(b []byte)(n int,error):从连接中读数据存到b中,返回的n是从连接中实际读取到的字节数和一个错误。
4.Conn.Write(b []byte)(n int,error):将b的数据写入连接中,返回的n是实际写入的字节数和一个错误。
5.Conn.Close() error :关闭连接,这是很有必要的,一般获取到Conn后都会写一个defer Conn,Close(),这样做是在函数完成的时候关闭连接,有助于资源管理和避免资源泄露。
6.net.Dial(“tcp”,address string)(Conn,error):创建一个TCP客户端连接到指定的地址。这个地址的格式也还是ip:port。
7.net.ResolveTCPAddr(network,address string)(*TCPAddr,error):解析一个TCP地址,并返回一个TCPAddr对象和一个错误
这个TCPAddr是什么:是net包中定义的结构体类型,用于表示TCP网络地址。具体定义如下:
type TCPAddr struct {
IP IP //IP地址
Port int //端口号
Zone string //表示域,在IPv6地址中可能有一个区域标识。
}
8.TCPConn.SetReadDeadline(t time.Time)error和TCPConn.SetWriteDeadline(t time.Time)error:设置读取或写入超时时间,超时后会返回一个net.Error类型的错误。
了解完上面之后就来看具体的TCP编程的实现,下面是有关使用net包实现TCP通信,TCP的通信模式是服务器和客户端,下面是一个服务器和客户端实例:
TCP服务器:
package main
import (
"fmt"
"net"
)
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("Error listening:", err.Error())
return
}
defer ln.Close()
fmt.Println("Server listening on port 8080")
for {
conn, err := ln.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err.Error())
continue
}
go handleRequest(conn)
}
}
func handleRequest(conn net.Conn) {
buf := make([]byte, 1024)
_, err := conn.Read(buf)
if err != nil {
fmt.Println("Error reading:", err.Error())
}
fmt.Println("Received message:", string(buf))
conn.Write([]byte("Hello from server!"))
conn.Close()
}
解读:
1.服务器创建了用net.Listen创建了一个监听器,网络类型是tcp,监听的地址是本地地址,端口号是8080.然后会返回一个net.Listener和error。进行错误判断。这里有一个监听器的关闭处理。
2.然后就是无限循环监听客户端的请求,就是调用Accept这个方法来实现。这个方法一旦调用就会返回一个Conn连接,对这个网络连接就可以做相关的操作。这里是启动了协程来处理这个连接
3.handleRequest请求处理函数对这个连接做了一个写入的操作。操作完了就关闭连接了。
这些函数了解清楚后很容易就可以看出这个代码的每一步。
总结服务器做了什么:打开监听器,监听请求得到连接,对连接进行处理,最后关闭连接关闭监听。
TCP客户端
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("Error connecting:", err.Error())
return
}
defer conn.Close()
message := "Hello from client!"
_, err = conn.Write([]byte(message))
if err != nil {
fmt.Println("Error sending message:", err.Error())
return
}
buf := make([]byte, 1024)
_, err = conn.Read(buf)
if err != nil {
fmt.Println("Error reading:", err.Error())
return
}
fmt.Println("Received message:", string(buf))
}
1.客户端使用dial向本地这个端口拨号建立tcp连接,建立之后返回连接
对这个连接做一个defer close的处理。
2.拿到这个连接后就可恶意做一些相关数据操作了,这里进行了一个写入操作,向服务器写了个hello from client。
3.之后还进行了向连接读取的操作,最后把读到的数据打印处理。这样就完事了。
总结客户端干什么:拨号建立连接,然后操作数据,操作完了之后关闭连接。
TCP流行框架
go语言有很多优秀的TCP网络编程框架,一下是一些比较流行的框架
1.gRPC:Google开源的高性能,跨语言,开箱即用的RPC框架。他基于HTTP/2协议,支持双向流、流控、多路复用等特性。除了提供基础的RPC功能外还提供了很多高级特性,比如拦截器,流式处理等,在Go语言中可以使用grpc-go库来使用gRPC。
2.Gorilla WebSocket:基于标准库net/http的WebSocket实现
3.Gnet:基于epoll实现
4.TcpServer:基于tcpserver-go库来使用。
golang游戏服务器框架
这里就随便介绍一下几个优秀的服务器框架,直到是个啥就行
1.Leaf
2.go-ethereum
3.nano
4.gonet
还有很多框架,这里就过一下知道有游戏服务器框架这东西就行。
golang udp编程
首先要了解api,这些全是net包下面的函数来实现UDP通信:
**ResolveUDPAddr(network,address string)(*UDPAddr,error)
**:解析UDP地址。该函数接受网络协议和地址,返回一个UDPAddr类型的结构体指针和一个可能出现的错误。network是指定网络类型,对于udp地址解析,这个地方通常是"udp",“udp4”,“udp6”,address是需要解析的地址,这个地址的形式是"ip:port"。
返回值类型:UDPAddr结构体指针
type UDPAddr struct {
IP IP //IP类型,IP类型是[]byte类型的别名,标识IP地址
Port int //端口号
Zone string // IPv6范围地址的区域
}
UDPAddr有什么用:用于标识UDP连接的端点。在UDP网络中,地址是进行数据发送和接受的基本单位。在服务器端,UDPAddr可用于指定服务器应该监听的IP地址和端口号。在客户端,他用于指定要将数据发送到的远程服务器的地址。
函数用途总结:在使用UDP协议进行网络编程时,ResolveUDPAddr用于获取UDP端点的地址,然后得到UDPAddr这个地址之后可以用于监听该端点或者向该端点发送数据。
实际例子:
比如我在创建一个UDP服务器,我要监听来自某个地址的UDP数据包,那么就必须先解析这个地址拿到UDPAddr,然后才能调用net.ListenUDP(“udp”,addr)来进行监听。
综上,这函数的功能就是解析地址。并不会创建网络连接或监听,而是用其他函数进行创建连接或监听,比如DialUDP,ListenUDP
2.typeUDPConn,UDP连接,该类型标识一个UDP连接,拿到这个连接就可以进行读写操作和关闭连接了,和TCP里面的net.Conn很像。
3.DialUDP(network string,laddr,raddr *UDPAddr)(*UDPConn,error):建立UDP连接,该函数接受本地地址和远程地址,第一个参数是网络类型,返回一个UDP连接和一个可能出现的错误。
laddr是本地地址,如果为nil,系统会自动选择一个本地地址和端口。raddr是远端地址,这个参数指定远程服务器的网络地址,在UDP中,这个通常就是你想把数据发到的地址。
*UDPConn是net包里面的一个结构体类型,这东西相当于TCP编程里面的Conn,拿到了UDPConn,就可以实现数据收发,连接关闭了,这些功能的实现也是基于这个结构体实现的方法来实现的,直接看这个下面这个定义
type UDPConn struct {
// 包含了内部用于网络通信的字段
// ...
}
// Read 从连接中读取数据
func (c *UDPConn) Read(b []byte) (int, error)
// Write 向连接写入数据
func (c *UDPConn) Write(b []byte) (int, error)
// Close 关闭连接
func (c *UDPConn) Close() error
// LocalAddr 返回本地网络地址
func (c *UDPConn) LocalAddr() Addr
// RemoteAddr 返回远程网络地址
func (c *UDPConn) RemoteAddr() Addr
// SetDeadline 设置读写操作的截止时间
func (c *UDPConn) SetDeadline(t time.Time) error
// SetReadDeadline 设置读操作的截止时间
func (c *UDPConn) SetReadDeadline(t time.Time) error
// SetWriteDeadline 设置写操作的截止时间
func (c *UDPConn) SetWriteDeadline(t time.Time) error
// ReadFromUDP 从连接中读取数据和发送者地址
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
// WriteToUDP 向指定的地址发送数据
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
// ... 可能还有其他方法
我去查了相关资料,说是字段细节隐藏了,但是说问题不大,因为用不上,而更应该关注它实现的方法。
上面这些函数应该都看得懂的,只要看了前面的内容。
4.**func ListenUDP(network string, laddr UDPAddr) (UDPConn, error)
用于创建UDP网络监听器,这个函数的主要作用是监听指定网络地址传入的UDP数据包。
network网络类型,这里udp那就udp,laddr是本地地址,你会发现这个地址还是*UDPAddr类型,这里注意一下。如果laddr的IP字段是nil或未指定,那么监听器会监听所有网络端口。
主要关注这个返回值,*UDPConn,就是服务器拿到连接了,也可以调用类型里面的方法进行数据收发了。
这函数主要用途来说:一般用于服务器接收指定端口上的UDP请求并对其进行处理。
实例
现在到了实例环节,udp的通信模式还是服务器和客户端。下面分别是udp客户端和服务器实现通信的例子:
udp客户端
package main
import (
"fmt"
"net"
"time"
)
var (
serverAddr = "localhost:8080"
)
func main() {
// 解析UDP地址
addr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
fmt.Println(err)
return
}
// 建立UDP连接
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 发送数据
msg := []byte("Hello, server!")
_, err = conn.Write(msg)
if err != nil {
fmt.Println(err)
return
}
// 接收响应
buf := make([]byte, 1024)
conn.SetReadDeadline(time.Now().Add(time.Second * 5)) // 设置读取超时时间
n, _, err := conn.ReadFromUDP(buf)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Received:", string(buf[:n]))
}
这个代码经过上面函数总结和应该都看得懂。
先net.ResolveUDPAddr()解析地址,得到UDPAddr
然后客户端对这个解析后的UDPAddr,进行拨号建立连接得到Conn,之后就可以调用这个UDPConn进行数据的发送和读,还要关闭连接。
这里也是进行了向连接发送数据和从连接接受数据。
服务器例子:
package main
import (
"fmt"
"net"
)
var (
serverAddr = "localhost:8080"
)
func main() {
// 解析UDP地址
addr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
fmt.Println(err)
return
}
// 建立UDP连接
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
fmt.Println("Server started on", serverAddr)
for {
// 接收请求
buf := make([]byte, 1024)
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("Received %d bytes from %s: %s\n", n, clientAddr, string(buf[:n]))
// 发送响应
_, err = conn.WriteToUDP(buf[:n], clientAddr)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("Sent %d bytes to %s\n", n, clientAddr)
}
}
解读:解析地址拿到*UDPAddr,然后调用UDPListen进行接听UDP请求获取UDPConn,然后服务器也可以进行数据操作了,服务器接收请求一般都会搞个循环处理,因为请求可能很多。这里看请求处理是先进行了从连接读入数据,然后向连接里写数据。
总结一下流程:udp都是先解析地址,这样才能拿到UDPAddr,因为无论是UDPListen还是UDPDial传入的参数都是UDPAddr,即解析后的地址,这样才能拿到连接进行操作。
学习时的疑问:
学tcp的时候我怎么感受不到?
解答:无论是TCP还是UDP解析地址都是一个重要步骤。但是UDP和TCP在使用上的差异,这才导致在UDP编程里面解析地址的步骤更加的明显。对于TCP编程,解析地址的细节已经隐藏在了net.Dial或net.Listen的内部实现中,Go的标准库内部会在创建TCP连接或监听器时自动的进行地址解析。
这种差异的主要原因是因为TCP是面向连接的而UDP无连接,所以地址解析直接包含在了建立连接的过程中,而UDP无连接,所以非常的需要在操作数据之前明确地址信息,因此地址解析变成了一个独立的步骤。