Golang 网络编程(一)

简介: Golang 网络编程(一)

TCP网络编程#


存在的问题:


  • 拆包:
  • 对发送端来说应用程序写入的数据远大于socket缓冲区大小,不能一次性将这些数据发送到server端就会出现拆包的情况。
  • 通过网络传输的数据包最大是1500字节,当TCP报文的长度 - TCP头部的长度 > MSS(最大报文长度时)将会发生拆包,MSS一般长(1460~1480)字节。


  • 粘包:
  • 对发送端来说:应用程序发送的数据很小,远小于socket的缓冲区的大小,导致一个数据包里面有很多不通请求的数据。
  • 对接收端来说:接收数据的方法不能及时的读取socket缓冲区中的数据,导致缓冲区中积压了不同请求的数据。


解决方法:

  • 使用带消息头的协议,在消息头中记录数据的长度。
  • 使用定长的协议,每次读取定长的内容,不够的使用空格补齐。
  • 使用消息边界,比如使用 \n 分隔 不同的消息。
  • 使用诸如 xml json protobuf这种复杂的协议。


实验:使用自定义协议


整体的流程:

客户端:发送端连接服务器,将要发送的数据通过编码器编码,发送。

服务端:启动、监听端口、接收连接、将连接放在协程中处理、通过解码器解码数据。


//###########################
//######  Server端代码  ###### 
//###########################
func main() {
  // 1. 监听端口 2.accept连接 3.开goroutine处理连接
  listen, err := net.Listen("tcp", "0.0.0.0:9090")
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  for{
    conn, err := listen.Accept()
    if err != nil {
      fmt.Printf("Fail listen.Accept : %v", err)
      continue
    }
    go ProcessConn(conn)
  }
}
// 处理网络请求
func ProcessConn(conn net.Conn) {
  defer conn.Close()
  for  {
    bt,err:=coder.Decode(conn)
    if err != nil {
      fmt.Printf("Fail to decode error [%v]", err)
      return
    }
    s := string(bt)
    fmt.Printf("Read from conn:[%v]\n",s)
  }
}
//###########################
//######  Clinet端代码  ###### 
//###########################
func main() {
  conn, err := net.Dial("tcp", ":9090")
  defer conn.Close()
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  // 将数据编码并发送出去
  coder.Encode(conn,"hi server i am here");
}
//###########################
//######  编解码器代码  ###### 
//###########################
/**
 *  解码:
 */
func Decode(reader io.Reader) (bytes []byte, err error) {
  // 先把消息头读出来
  headerBuf := make([]byte, len(msgHeader))
  if _, err = io.ReadFull(reader, headerBuf); err != nil {
    fmt.Printf("Fail to read header from conn error:[%v]", err)
    return nil, err
  }
  // 检验消息头
  if string(headerBuf) != msgHeader {
    err = errors.New("msgHeader error")
    return nil, err
  }
  // 读取实际内容的长度
  lengthBuf := make([]byte, 4)
  if _, err = io.ReadFull(reader, lengthBuf); err != nil {
    return nil, err
  }
  contentLength := binary.BigEndian.Uint32(lengthBuf)
  contentBuf := make([]byte, contentLength)
  // 读出消息体
  if _, err := io.ReadFull(reader, contentBuf); err != nil {
    return nil, err
  }
  return contentBuf, err
}
/**
 *  编码
 *  定义消息的格式: msgHeader + contentLength + content
 *  conn 本身实现了 io.Writer 接口
 */
func Encode(conn io.Writer, content string) (err error) {
  // 写入消息头
  if err = binary.Write(conn, binary.BigEndian, []byte(msgHeader)); err != nil {
    fmt.Printf("Fail to write msgHeader to conn,err:[%v]", err)
  }
  // 写入消息体长度
  contentLength := int32(len([]byte(content)))
  if err = binary.Write(conn, binary.BigEndian, contentLength); err != nil {
    fmt.Printf("Fail to write contentLength to conn,err:[%v]", err)
  }
  // 写入消息
  if err = binary.Write(conn, binary.BigEndian, []byte(content)); err != nil {
    fmt.Printf("Fail to write content to conn,err:[%v]", err)
  }
  return err


客户端的conn一直不被Close 有什么表现?


四次挥手各个状态的如下:


主从关闭方           被动关闭方
established         established
Fin-wait1         
                    closeWait
Fin-wait2
Tiem-wait           lastAck
Closed              Closed


如果客户端的连接手动的关闭,它和服务端的状态会一直保持established建立连接中的状态。


MacBook-Pro% netstat -aln | grep 9090
tcp4       0      0  127.0.0.1.9090         127.0.0.1.62348        ESTABLISHED
tcp4       0      0  127.0.0.1.62348        127.0.0.1.9090         ESTABLISHED
tcp46      0      0  *.9090                 *.*                    LISTEN


服务端的conn一直不被关闭 有什么表现?


客户端的进程结束后,会发送fin数据包给服务端,向服务端请求断开连接。


服务端的conn不关闭的话,服务端就会停留在四次挥手的close_wait阶段(我们不手动Close,服务端就任务还有数据/任务没处理完,因此它不关闭)。


客户端停留在 fin_wait2的阶段(在这个阶段等着服务端告诉自己可以真正断开连接的消息)。


MacBook-Pro% netstat -aln | grep 9090
tcp4       0      0  127.0.0.1.9090         127.0.0.1.62888        CLOSE_WAIT
tcp4       0      0  127.0.0.1.62888        127.0.0.1.9090         FIN_WAIT_2
tcp46      0      0  *.9090                 *.*                    LISTEN


什么是binary.BigEndian?什么是binary.LittleEndian?


对计算机来说一切都是二进制的数据,BigEndian和LittleEndian描述的就是二进制数据的字节顺序。计算机内部,小端序被广泛应用于现代性 CPU 内部存储数据;大端序常用于网络传输和文件存储。


比如:


一个数的二进制表示为   0x12345678
BigEndian   表示为: 0x12 0x34 0x56 0x78 
LittleEndian表示为: 0x78 0x56 0x34 0x12


UDP网络编程#


思路:

UDP服务器:1、监听 2、循环读取消息 3、回复数据。

UDP客户端:1、连接服务器 2、发送消息 3、接收消息。


// ################################
// ######## UDPServer #########
// ################################
func main() {
  // 1. 监听端口 2.accept连接 3.开goroutine处理连接
  listen, err := net.Listen("tcp", "0.0.0.0:9090")
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  for{
    conn, err := listen.Accept()
    if err != nil {
      fmt.Printf("Fail listen.Accept : %v", err)
      continue
    }
    go ProcessConn(conn)
  }
}
// 处理网络请求
func ProcessConn(conn net.Conn) {
  defer conn.Close()
  for  {
    bt,err:= coder.Decode(conn)
    if err != nil {
      fmt.Printf("Fail to decode error [%v]", err)
      return
    }
    s := string(bt)
    fmt.Printf("Read from conn:[%v]\n",s)
  }
}
// ################################
// ######## UDPClient #########
// ################################
func main() {
  udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{
    IP:   net.IPv4(127, 0, 0, 1),
    Port: 9091,
  })
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  _, err = udpConn.Write([]byte("i am udp client"))
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  bytes:=make([]byte,1024)
  num, addr, err := udpConn.ReadFromUDP(bytes)
  if err != nil {
    fmt.Printf("Fail to read from udp error: [%v]", err)
    return
  }
  fmt.Printf("Recieve from udp address:[%v], bytes:[%v], content:[%v]",addr,num,string(bytes))
}


Http网络编程#


思路整理:

HttpServer:1、创建路由器。2、为路由器绑定路由规则。3、创建服务器、监听端口。 4启动读服务。


HttpClient: 1、创建连接池。2、创建客户端,绑定连接池。3、发送请求。4、读取响应。


func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/login", doLogin)
  server := &http.Server{
    Addr:         ":8081",
    WriteTimeout: time.Second * 2,
    Handler:      mux,
  }
  log.Fatal(server.ListenAndServe())
}
func doLogin(writer http.ResponseWriter,req *http.Request){
  _, err := writer.Write([]byte("do login"))
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
}


HttpClient端


func main() {
  transport := &http.Transport{
    // 拨号的上下文
    DialContext: (&net.Dialer{
      Timeout:   30 * time.Second, // 拨号建立连接时的超时时间
      KeepAlive: 30 * time.Second, // 长连接存活的时间
    }).DialContext,
    // 最大空闲连接数
    MaxIdleConns:          100,  
    // 超过最大的空闲连接数的连接,经过 IdleConnTimeout时间后会失效
    IdleConnTimeout:       10 * time.Second, 
    // https使用了SSL安全证书,TSL是SSL的升级版
    // 当我们使用https时,这行配置生效
    TLSHandshakeTimeout:   10 * time.Second, 
    ExpectContinueTimeout: 1 * time.Second,  // 100-continue 状态码超时时间
  }
  // 创建客户端
  client := &http.Client{
    Timeout:   time.Second * 10, //请求超时时间
    Transport: transport,
  }
  // 请求数据
  res, err := client.Get("http://localhost:8081/login")
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  defer res.Body.Close()
  bytes, err := ioutil.ReadAll(res.Body)
  if err != nil {
    fmt.Printf("error : %v", err)
    return
  }
  fmt.Printf("Read from http server res:[%v]", string(bytes))
}


理解函数是一等公民#


点击查看在github中函数相关的笔记


在golang中函数是一等公民,我们可以把一个函数当作普通变量一样使用。

比如我们有个函数HelloHandle,我们可以直接使用它。


func HelloHandle(name string, age int) {
  fmt.Printf("name:[%v] age:[%v]", name, age)
}
func main() {
  HelloHandle("tom",12)
}


闭包


如何理解闭包:闭包本质上是一个函数,而且这个函数会引用它外部的变量,如下例子中的f3中的匿名函数本身就是一个闭包。 通常我们使用闭包起到一个适配的作用。

例1:


// f2是一个普通函数,有两个入参数
func f2() {
  fmt.Printf("f2222")
}
// f1函数的入参是一个f2类型的函数
func f1(f2 func()) {
  f2()
}
func main() {
  // 由于golang中函数是一等公民,所以我们可以把f2同普通变量一般传递给f1
  f1(f2)
}


例2: 在上例中更进一步。f2有了自己的参数, 这时就不能直接把f2传递给f1了。


总不能傻傻的这样吧f1(f2(1,2)) ???

而闭包就能解决这个问题。


// f2是一个普通函数,有两个入参数
func f2(x int, y int) {
  fmt.Println("this is f2 start")
  fmt.Printf("x: %d y: %d \n", x, y)
  fmt.Println("this is f2 end")
}
// f1函数的入参是一个f2类型的函数
func f1(f2 func()) {
  fmt.Println("this is f1 will call f2")
  f2()
  fmt.Println("this is f1 finished call f2")
}
// 接受一个两个参数的函数, 返回一个包装函数
func f3(f func(int,int) ,x,y int) func() {
  fun := func() {
    f(x,y)
  }
  return fun
}
func main() {
  // 目标是实现如下的传递与调用
  f1(f3(f2,6,6))
}


实现方法的回调:


下面的例子中实现这样的功能:就好像是我设计了一个框架,定好了整个框架运转的流程(或者说是提供了一个编程模版),框架具体做事的函数你根据自己的需求自己实现,我的框架只是负责帮你回调你具体的方法。


// 自定义类型,handler本质上是一个函数
type HandlerFunc func(string, int)
// 闭包
func (f HandlerFunc) Serve(name string, age int) {
  f(name, age)
}
// 具体的处理函数
func HelloHandle(name string, age int) {
  fmt.Printf("name:[%v] age:[%v]", name, age)
}
func main() {
  // 把HelloHandle转换进自定义的func中
  handlerFunc := HandlerFunc(HelloHandle)
  // 本质上会去回调HelloHandle方法
  handlerFunc.Serve("tom", 12)
  // 上面两行效果 == 下面这行
  // 只不过上面的代码是我在帮你回调,下面的是你自己主动调用
  HelloHandle("tom",12)
}


相关文章
|
6月前
|
Go
golang力扣leetcode 2039.网络空闲的时刻
golang力扣leetcode 2039.网络空闲的时刻
30 0
|
监控 网络协议 Go
Golang抓包:实现网络数据包捕获与分析
Golang抓包:实现网络数据包捕获与分析
|
5月前
|
Go
golang读取网络字节并解压zip
golang读取网络字节并解压zip
44 0
|
6月前
|
存储 网络协议 Go
Golang网络聊天室案例
Golang网络聊天室案例
68 2
Golang网络聊天室案例
|
安全 Java Go
Golang出现泛型后,Gin怎么封装网络请求处理
Go 1.18后出现泛型,小白怎么使用Gin框架怎么根据泛型封装客户端请求,
547 0
|
网络协议 前端开发 Go
Golang 网络编程(三)
Golang 网络编程(三)
291 0
|
网络协议 安全 Java
Golang 网络编程(二)
Golang 网络编程(二)
166 0
|
运维 监控 网络协议
golang 服务诡异499、504网络故障排查
事故经过 排查 总结 事故经过 11-01 12:00 中午午饭期间,手机突然收到业务网关非200异常报警,平时也会有一些少量499或者网络抖动问题触发报警,但是很快就会恢复(目前配置的报警阈值是5%,阈值跟当时的采样窗口qps有直接关系)。
5206 0
|
负载均衡 网络协议 前端开发
【开源】gnet: 一个轻量级且高性能的 Golang 网络库
gnet 是一个基于 Event-Loop 事件驱动的高性能和轻量级网络库。这个库直接使用 epoll 和 kqueue 系统调用而非标准 Golang 网络包:net 来构建网络应用,它的工作原理类似于两个开源的网络库:libuv 和 libevent。
3379 0