Go语言 Socket编程

Go语言

19. Socket编程

Socket是在使用Go语言的过程中会使用到的最底层的网络协议,大部分的网络通信协议都是基于TCP/IP的Socket协议。

19.1 计算机网络简介

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备通过通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

19.1.1 OSI七层网络模型

OSI(Open System Interconnection,开放式系统互联)是一个开放性的通信系统互联参考模型。OSI模型有七层结构,每层都可以有几个子层。OSI的七层从上到下分别是:7-应用层、6-表示层、5-会话层、4-传输层、3-网络层、2-数据链路层、1-物理层。其中高层(即7、6、5、4层)定义了应用程序的功能,下面三层(即3、2、1层)主要面向通过网络的端到端的数据流。

应用层
表示层
会话层
传输层
网络层
数据链路层
物理层

网络中的计算机与终端间要想正确地传送信息和数据,必须在数据传输的顺序、数据的格式及内容等方面有一个约定或规则,这种约定或规则称作协议(protocol)。OSI的每一层都是为了完成各自功能,为了实现这些功能,就需要大家都遵守共同的协议。

  • 应用层

    这一层是最靠近用户的OSI层,为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。常见的协议有:HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP。

  • 表示层

    表示层可确保一个系统的应用层所发送的信息能够被另一个系统的应用层读取。

    常见的协议有:JPEG、ASCII、DECOIC。

  • 会话层

    会话层通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。

    常见的协议有:RPC、SCP、SSH、ZIP。

  • 传输层

    传输层定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高、数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高、数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。传输层主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫作段。

    常见的协议有:TCP、UDP。

  • 网络层

    网络层为位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。

    常见的协议有:ICMP、IGMP、IP、RARP。

  • 数据链路层

    数据链路层定义了如何格式化数据以进行传输,以及如何控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。

    常见的协议有:PPP、IEEE 802.3/802.2。

  • 物理层

    物理层主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫作比特。

    常见的协议有:Ethernet 802.3。

大多数的计算机网络都采用层次式结构,即将一个计算机网络分为若干层次,处在高层次的系统仅是利用较低层次的系统提供的接口和功能,无须了解低层实现该功能所采用的算法和协议;较低层次系统也仅是使用从高层系统传送来的参数,这就是层次间的无关性。因为有了这种无关性,层次间的每个模块可以用一个新的模块取代,只要新的模块与旧的模块具有相同的功能和接口,即使它们使用的算法和协议都不一样也没关系。

网络分层的好处:

  • 人们可以很容易地讨论和学习协议的规范细节
  • 层间的标准接口方便了工程模块化
  • 创建了一个更好的互联环境
  • 降低了复杂度,使程序更容易被修改,产品开发的速度更快
  • 每层利用紧邻的下层服务,更容易记住各层的功能
19.1.2 TCP/IP协议

互联网协议族(Internet Protocol Suite)是一个网络通信模型,包括整个网络传输协议家族,是互联网的基础通信架构。它常被通称为TCP/IP协议族(TCP/IP Protocol Suite,或TCP/IP Protocols),简称为TCP/IP,这是因为该协议家族的两个核心协议TCP(Transmission Control Protocol,传输控制协议)和IP(Internet Protocol,网际互连协议)是该家族中最早通过的标准。

TCP/IP提供点对点的链接机制,将数据应该如何封装、定址、传输、路由以及在目的地如何接收,都加以标准化。它将软件通信过程抽象为四个抽象层,采取协议堆栈的方式,分别实现不同的通信协议。协议族下的各种协议,依其功能不同,被分别归属到这四个层次结构之中,常被视为简化的OSI七层模型。

在这里插入图片描述

TCP/IP参考模型是首先由ARPANET所使用的网络体系结构。这个体系结构在它的两个主要协议出现以后被称为TCP/IP参考模型(TCP/IP Reference Model)。这一网络协议共分为四层:网络访问层、网络层、传输层和应用层。

  • 网络访问层(Network Access Layer)

    在TCP/IP参考模型中并没有详细描述,只是指出主机必须使用某种协议与网络相连。

  • 网络层(Internet Layer)

    功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。

  • 传输层(Transport Layer)

    传输层使源端和目的端机器上的对等实体可以进行会话。

  • 应用层(Application Layer)

    应用层包含所有的高层协议,包括远程终端协议(TELNET)、文件传输协议(FTP)、电子邮件传输协议(SMTP)、域名服务(DNS)、网络新闻传输协议(NNTP)和超文本传输协议(HTTP)等。

19.1.3 IPv4和IPv6

IPv4是Internet Protocol Version 4的缩写,即网际互连协议的第四版,也是第一个被广泛使用、构成现今互联网技术的基石的协议。

IPv6是Internet Protocol Version 6的缩写,它是IETF(The Internet Engineering Task Force,互联网工程任务组)设计的用于替代现行版本IP协议(IPv4)的下一代IP协议。与IPv4相比,IPv6具有以下几个优势:

  • IPv6具有更大的地址空间。IPv4中规定IP地址长度为32,即有2^32-1个地址;而IPv6中IP地址
    的长度为128,即有2^128-1个地址。
  • IPv6使用更小的路由表。IPv6的地址分配一开始就遵循聚类(Aggregation)的原则,这使路由器能在路由表中用一条记录(Entry)表示一片子网,大大减小了路由器中路由表的长度,提高了路由器转发数据包的速度。
  • IPv6增加了增强的组播(Multicast)支持以及对流的控制(Flow Control),这使网络上的多媒体应用有了长足发展的机会,为服务质量(QoS,Quality of Service)控制提供了良好的网络平台。
  • IPv6加入了对自动配置(Auto Configuration)的支持。这是对DHCP协议的改进和扩展,使网络(尤其是局域网)的管理更加方便和快捷。
  • IPv6具有更高的安全性。在使用IPv6网络时用户可以对网络层的数据进行加密并对IP报文进行校验,极大增强了网络的安全性。
19.1.4 子网掩码

子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,它是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。

利用子网掩码可以把大的网络划分成子网,即VLSM(可变长子网掩码),也可以把小的网络归并成大的网络,即超网。

19.2 Socket基础

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。

建立网络通信连接至少要一对端口号(Socket)。Socket的本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。

19.2.1 Socket简介

Socket的英文原义是“孔”或“插座”,作为BSD UNIX的进程通信机制,取后一种意思。Socket通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。

在网络上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。

Socket起源于Unix,而Unix的基本哲学之一就是“一切皆文件”,都可以使用如下模式来操作。

打开 -> 读写write/read -> 关闭close

Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件
描述符。Socket的类型有两种:流式Socket和数据报式Socket。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

19.2.2 netcat工具简介

命令行工具nc,全称netcat,在测试TCP/IP的客户端和服务端时非常便利。

nc可以作为TCP服务的客户端使用,例如作为客户端连接监听在本机127.0.0.1上端口为1234的服务:

nc 127.0.0.1 1234

命令行工具nc默认使用TCP协议。如果使用UDP协议,可以在执行nc命令时携带-u参数。

nc-u 127.0.0.1 1234

如果要使用nc模拟服务器,可以携带-l参数,nc将监听指定端口号的连接,例如在本机的1234端口建立一个TCP服务端。

nc-l-p 1234

同样,建立UDP服务器只需加-u参数。

nc-l-u-p 1234
nc-lup 1234(简写)

如果希望netcat(1)生成详细的输出,可以使用-v和-vv参数,这些输出为排查网络连接故障提供
了很大的便利。

nc-v 127.0.0.1 1234
19.3 TCP编程
19.3.1 TCP简介

TCP是Transmission Control Protocol的缩写,中文名是传输控制协议。它是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的SYN执行ACK确认。这种建立连接的方法可以防止产生错误的连接。TCP使用的流量控制协议是可变大小的滑动窗口协议。

TCP三次握手的过程:

  1. 客户端发送SYN(seq=J)报文给服务器端,进入SYN_SENT状态;
  2. 服务器端收到SYN报文,回应一个SYN(seq=K)+ACK(ack=J+1)报文,进入SYN_RCVD状态;
  3. 客户端收到服务器端的SYN报文,回应一个ACK(ack=K+1)报文,进入ESTABLISHED状态。

在这里插入图片描述

三次握手完成,TCP客户端和服务器端成功地建立连接,便可以开始传输数据了。

TCP通过下列方式来提供可靠性:

  1. 应用数据被分割成TCP认为最适合发送的数据块。
  2. 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。
  3. TCP将保持它首部和数据的检验和。
  4. 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。
  5. 既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
  6. TCP还能提供流量控制。
19.3.2 TCP客户端

Go语言提供了net包来实现Socket编程,大部分使用者只需要Dial、Listen和Accept函数提供的基本接口,以及相关的Conn和Listener接口。创建一个TCP客户端只需使用Dial函数即可。

func Dial(network, address string) (Conn, error)

在网络network上连接地址address,并返回一个Conn接口。可用的网络类型有:tcp、tcp4、tcp6、udp、udp4、udp6、ip、ip4、ip6、unix、unixgram、unixpacket。

对于TCP和UDP网络,地址格式是“host:port”或“[host]:port”:

Dial("tcp", "12.34.56.78:80")
Dial("tcp", "google.com:http")
Dial("tcp", "[2001:db8::1]:http")
Dial("tcp", "[fe80::1%lo0]:80")

对于IP网络,network必须是ip、ip4、ip6后跟冒号和协议号或者协议名,地址必须是IP地址字面值:

Dial("ip4:1", "127.0.0.1")
Dial("ip6:ospf", "::1")

使用Dial函数尝试 连接百度服务器

package main

import (
   "log"
   "net"
)

func main() {
   conn, err := net.Dial("tcp", "www.baidu.com:80")
   if err != nil {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功!")
}

在这里插入图片描述

对于网络编程而言,推荐使用log包代替fmt包进行打印信息,log包打印时,会附加打印出时间,方便我们调试程序。log.Fatal表示当遇到严重错误时打印错误信息,并停止程序的运行。

Dial函数在连接时,如果端口未开放,尝试连接就会立刻返回服务器拒绝连接的错误。

package main

import (
   "log"
   "net"
)

func main() {
   //尝试连接本地1234端口
   conn, err := net.Dial("tcp", ":1234")
   if err != err {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功 !")
}

在这里插入图片描述

来避免程序一直阻塞运行,设置超时可以使用DialTimeout函数。

func DialTimeout(network, address string, timeout time.Duration) 
(Conn, error)
package main

import (
   "log"
   "net"
   "time"
)

func main() {
   //设置超时
   conn, err := net.DialTimeout("tcp", "www.baidu.com:81", time.Second*3)
   if err != nil {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功!")
}

在这里插入图片描述

建立TCP连接后,就会进行数据的传输。数据传输时使用Read和Write函数分别从连接中读取(接收)和写入(发送)数据。

Read从连接中读取数据,Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真。

Read(b []byte) (n int, err error)

Write从连接中写入数据,Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真。

Write(b []byte) (n int, err error)
package main

import (
   "log"
   "net"
)

func main() {
   conn, err := net.Dial("tcp", ":1234")
   if err != nil {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功!")
   //发送数据
   conn.Write([]byte("test\n"))
   //接收数据
   var buf = make([]byte, 10)
   conn.Read(buf)
   log.Println(buf)
}

与百度服务器进行数据通信

package main

import (
   "log"
   "net"
)

func main() {
   //尝试连接百度服务器
   conn, err := net.Dial("tcp", "www.baidu.com:80")
   if err != nil {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功!")
   //发送HTTP形式的内容
   conn.Write([]byte("GET / HTTP/1.1\r\nHost: www.baidu.com\r\nUser-Agent: curl/7.55.1\r\nAccept: */*\r\n\r\n"))

   var buf = make([]byte, 1024)
   conn.Read(buf)
   log.Println(string(buf))
}

在这里插入图片描述

19.3.3 TCP服务端

创建一个TCP服务端只需使用Listen函数即可。

func Listen(net, laddr string) (Listener, error)

Listen函数返回在一个本地网络地址laddr上监听的Listener。网络类型参数net必须是面向流的网络:tcp、tcp4、tcp6、unix或unixpacket。可见,Listen是无法创建一个UDP服务器的,因为UDP是面向数据报式的网络(无连接)。

package main

import (
   "log"
   "net"
)

func main() {
   //监听8080端口
   l, err := net.Listen("tcp", ":8080")
   if err != nil {
      log.Fatal("服务启动失败", err)
   }
   defer l.Close()
   log.Println("服务启动成功!")
}

在创建了一个TCP服务器后,就需要使用Accept函数获取和处理来自客户端的连接。

func (l *TCPListener) Accept() (Conn, error)

Accept用于实现Listener接口的Accept方法,它会等待下一个呼叫,并返回一个该呼叫的Conn接口。

func (c *TCPConn) SetDeadline(t time.Time) error

SetDeadline设置读写操作期限,设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline。deadline是一个绝对时间,超过该时间后,I/O操作就会直接因超时失败返回而不会阻塞。deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作。参数t为零值时表示不设置期限。

func (c *TCPConn) SetReadDeadline(t time.Time) error

SetReadDeadline设置读操作期限,设定该连接的读操作deadline,参数t为零值时表示不设置期限。

func (c *TCPConn) SetWriteDeadline(t time.Time) error

SetWriteDeadline设置写操作期限,设定该连接的写操作deadline,参数t为零值时表示不设置期限。即使写入超时,返回值n也可能>0,说明成功写入了部分数据。

package main

import (
   "log"
   "net"
   "time"
)

func main() {
   //监听8080端口
   l, err := net.Listen("tcp", ":8080")
   if err != nil {
      log.Fatal("服务启动失败!", err)
   }
   defer l.Close()
   log.Println("服务启动成功!")
   //阻塞等待用户连接
   c, err := l.Accept()
   //设置连接超时时间
   c.SetDeadline(time.Now().Add(time.Second))
   //设置读取超时时间
   c.SetReadDeadline(time.Now().Add(time.Second))
   //设置写入超时时间
   c.SetWriteDeadline(time.Now().Add(time.Second))
}

在这里插入图片描述

对于服务端而言,接收和发送数据也是使用Read和Write函数。

package main

import (
   "log"
   "net"
)

func checkErr(err error) {
   if err != nil {
      log.Fatal(err)
   }
}

func main() {
   //监听8080端口
   l, err := net.Listen("tcp", ":8080")
   checkErr(err)
   defer l.Close()
   log.Println("服务启动 成功")
   //阻塞等待用户连接
   c, err := l.Accept()
   checkErr(err)

   defer c.Close()
   //读取打印接收的信息
   var buf = make([]byte, 10)
   log.Println("Start to read from conn")
   n, err := c.Read(buf)

   checkErr(err)
   log.Println("接收字节数:", n, "接收字内容为;", string(buf))

}

在这里插入图片描述

使用for循环持续不断地接收客户端的请求,并且可以给每一个请求使用新的协程来处理,提高并发能力。

package main

import (
   "log"
   "net"
)

func checkErr(err error) {
   if err != nil {
      log.Fatal(err)
   }
}

func handleConn(c net.Conn) {
   defer c.Close()
   for {
      var buf = make([]byte, 10)
      log.Println("开始从conn读取:")

      n, err := c.Read(buf)
      checkErr(err)
      log.Printf("read %d bytes ,content is : %s\n", n, string(buf[:n]))
   }
}

func main() {
   //监听8080端口
   l, err := net.Listen("tcp", ":8080")
   checkErr(err)
   defer l.Close()
   log.Println("服务启动 成功")

   for {
      c, err := l.Accept()
      checkErr(err)
      //开启新协程处理连接
      go handleConn(c)
   }

}

在这里插入图片描述

19.4 UDP编程
19.4.1 UDP简介

UDP是User Datagram Protocol的缩写,中文名是用户数据报协议。它是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

UDP用来支持那些需要在计算机之间传输数据的网络应用,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用。UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今天,UDP仍然不失为一项非常实用和可行的网络传输层协议。

19.4.2 UDP客户端

与TCP客户端类似,创建一个UDP客户端同样使用Dial函数,只需在参数中声明发起的请求协议为UDP即可。

net.Dial("udp", ":1234")
package main

import (
   "log"
   "net"
)

func main() {
   //尝试连接本地1234端口
   conn, err := net.Dial("udp", ":1234")
   if err != nil {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功!")
}

在这里插入图片描述

由于UDP是无连接的协议,只关心信息是否成功发送,不关心对方是否成功接收,只要消息报文发送成功,就不会报错,因此会输出连接成功的信息。

创建一个UDP客户端还可以使用DialUDP函数,该函数会用到ResolveUDPAddr函数来获取一个UDP的地址。

func ResolveUDPAddr(net, addr string) (*UDPAddr, error)

ResolveUDPAddr将addr作为UDP地址解析并返回。参数addr格式为“host:port”或“[ipv6-host%zone]:port”,解析得到网络名和端口名;net必须是“udp”“udp4”或“udp6”。IPv6地址字面值/名称必须用方括号包起来,如“[::1]:80”“[ipv6-host]:http”或“[ipv6-host%zone]:80”。

func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)

DialUDP在网络协议net上连接本地地址laddr和远端地址raddr。net必须是“udp”“udp4”“udp6”;如果laddr不是nil,将使用它作为本地地址,否则自动选择一个本地地址。

package main

import (
   "log"
   "net"
)

func main() {
   //创建一个udp地址
   udpaddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:8080")
   //建立连接
   conn, err := net.DialUDP("udp", nil, udpaddr)
   if err != nil {
      log.Fatal("连接失败!", err)
   }
   defer conn.Close()
   log.Println("连接成功!")
}

在这里插入图片描述

UDP的接收和发送操作,使用Read函数进行接收,使用Write函数进行发送

package main

import (
   "log"
   "net"
)

func checkErr(err error) {
   if err != nil {
      log.Fatal(err)
   }
}

func main() {
   //创建一个UDP地址
   udpaddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:1234")
   checkErr(err)
   //建立连接
   conn, err := net.DialUDP("udp", nil, udpaddr)

   checkErr(err)
   defer conn.Close()
   log.Println("连接成功!")
   //发送数据
   conn.Write([]byte("Hello\r\n"))
   //接收数据
   var buf = make([]byte, 1024)
   conn.Read(buf)
}

在这里插入图片描述

19.4.3 UDP服务端

与TCP服务端不同,创建一个UDP服务端无法使用有连接的Listen函数,而要使用无连接的ListenUDP函数。

func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)

ListenUDP创建一个接收目的地是本地地址laddr的UDP数据包的网络连接。net必须是“udp”“udp4”“udp6”;如果laddr端口为0,函数将选择一个当前可用的端口,可以用Listener的Addr方法获得该端口。返回的*UDPConn的ReadFrom和WriteTo方法可以用来发送和接收UDP数据包(每个包都可获得来源地址或设置目标地址)。

创建UDP服务

package main

import (
   "log"
   "net"
)

func checkErr(err error) {
   if err != nil {
      log.Fatal(err)
   }
}

func main() {
   //创建一个UDP地址
   udpaddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1234")
   checkErr(err)
   //创建UDP服务
   conn, err := net.ListenUDP("udp", udpaddr)
   checkErr(err)
   defer conn.Close()
   log.Println("UDP服务创建成功!")
}

在这里插入图片描述

对于UDP服务端的接收操作还是使用Read函数,发送操作则是使用WriteToUDP函数,其中addr表示目标客户端的地址,可以使用ReadFromUDP函数获取。

func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

WriteToUDP通过c向地址addr发送一个数据包,b为包的有效负载,返回写入的字节。WriteToUDP方法会在超过一个固定的时间点之后超时,并返回一个错误。在面向数据包的连接上,写入超时是十分罕见的。

func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)

ReadFromUDP从c读取一个UDP数据包,将有效负载拷贝到b,返回拷贝字节数和数据包来源地址。ReadFromUDP方法会在超过一个固定的时间点之后超时,并返回一个错误。

package main

import (
   "log"
   "net"
)

func checkErr(err error) {
   if err != nil {
      log.Fatal(err)
   }
}

func main() {
   //创建一个UDP地址
   udpaddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:1234")
   checkErr(err)
   //创建UDP服务
   conn, err := net.ListenUDP("udp", udpaddr)
   checkErr(err)
   defer conn.Close()
   log.Println("UDP服务创建成功!")

   var buf = make([]byte, 1024)
   conn.Read(buf)
   log.Println(string(buf))

   _, raddr, err := conn.ReadFromUDP(buf)
   conn.Write([]byte("Hello Write\r\n"))
   conn.WriteToUDP([]byte("Hello WriteToUDP\r\n"), raddr)
}

在这里插入图片描述

基于UDP的协议有很多,如DNS域名解析服务、NTP网络时间协议等。

模拟NTP服务器,每当接收到任意字节的信息,就将当前的时间发送给客户端。

package main

import (
   "log"
   "net"
   "time"
)

func checkErr(err error) {
   if err != nil {
      log.Fatal(err)
   }
}

func handleClient(conn *net.UDPConn) {
   data := make([]byte, 1024)
   n, remoteAddr, err := conn.ReadFromUDP(data)
   checkErr(err)
   log.Println(n, remoteAddr)
   b := make([]byte, 1024)
   b = []byte(string(time.Now().String()))
   conn.WriteToUDP(b, remoteAddr)
}

func main() {
   //创建UDP地址
   udpaddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:1234")
   checkErr(err)
   //创建UDP服务
   conn, err := net.ListenUDP("udp", udpaddr)
   checkErr(err)

   defer conn.Close()
   log.Println("UDP服务创建成功!")
   for {
      handleClient(conn)
   }
}

在这里插入图片描述

19.6 知识拓展

实现一个批量端口扫描器,主要针对TCP的端口进行扫描,通过命令行的方式指定端口,扫描获取端口是否开放

指定如“80,443,1000-2000”这种格式的批量端口扫描器

package main

import (
   "flag"
   "log"
   "net"
   "strconv"
   "strings"
   "sync"
   "time"
)

func processPortItem(port string) []string {
   var ports []string
   arr := strings.Split(port, ",")
   for _, p := range arr {
      if strings.Contains(p, "-") {
         ports = append(ports, rangeToArr(p)...)
      } else {
         ports = append(ports, p)
      }
   }
   return ports
}

func rangeToArr(s string) []string {
   if strings.Contains(s, "-") {
      var arr []string
      from, _ := strconv.Atoi(strings.Split(s, "-")[0])
      to, _ := strconv.Atoi(strings.Split(s, "-")[1])
      if from == 0 {
         from = 1
      }
      if to == 0 {
         to = 65535
      }
      for i := from; i <= to; i++ {
         arr = append(arr, strconv.Itoa(i))
      }
      return arr
   } else {
      return []string{s}
   }
}

func scan(ip string, port string, wg *sync.WaitGroup) {
   conn, err := net.DialTimeout("tcp", ip+":"+port, time.Second)
   if err != nil {
      wg.Done()
      return
   }
   wg.Done()
   defer conn.Close()
   log.Println(ip, port, "端口开放!")
}

func main() {
   ip := flag.String("h", "127.0.0.1", "指定主机IP")
   port := flag.String("p", "1-1000", "指定扫描的端口")
   flag.Parse()
   log.Println("扫描的端口为:", *ip, *port)

   //线程同步
   wg := &sync.WaitGroup{}

   for _, p := range processPortItem(*port) {
      wg.Add(1)
      go scan(*ip, p, wg)
   }
   wg.Wait()
}

在这里插入图片描述

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ding Jiaxiong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值