【Go网络编程】tcp与udp

73 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

传输层

学习过计算机网络的同学应该知道,运算层位于网际层与应用层之间,基于IP协议实现跨设备的交流,为应用层提供可靠与非可靠的数据运输。它相当于海边的港口,控制网络数据的收发与整理。

  • 传输控制协议 TCP(Transmission Control Protocol)–提供面向连接的,可靠的数据传输服务。
  • 用户数据协议 UDP(User Datagram Protocol)–提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

网络编程

监听端口与建立连接

想要写一个tcp、udp通信代码,需要明白必须有两个组成部分:监听者/服务端(监听端口的信息)、客户端(主动连接服务端,进行数据交换)。服务端像常驻运行,随时等待客户端的连接;客户端在需要连接时才创建,传输完数据后会关闭;下面分别对tcp与udp实现一个发送消息并返回消息的最简单例子:

TCP

// 为方便阅读,去除错误处理部分代码
func Test_tcp_server1(t *testing.T) {
	listen, _ := net.Listen("tcp", "127.0.0.1:8848")
	defer listen.Close()

	conn, _ := listen.Accept() // 建立连接

	msg := make([]byte, 128)
	n, _ := conn.Read(msg)
	fmt.Println(string(msg[:n]))

	conn.Write([]byte("get it!"))
}

func Test_tcp_client1(t *testing.T) {
	conn, _ := net.Dial("tcp", "127.0.0.1:8848")
	defer conn.Close() // 关闭连接

	msg := "helllo word!"
	conn.Write([]byte(msg)) // 发送数据

	buf := [512]byte{}
	n, _ := conn.Read(buf[:])

	fmt.Println(string(buf[:n]))
}

运行结果:

=== RUN   Test_tcp_server1
helllo word!
--- PASS: Test_tcp_server1 (4.04s)

=== RUN   Test_tcp_client1
get it!
--- PASS: Test_tcp_client1 (0.00s)

执行流程:

  1. 启动server,监听端口,阻塞在listen.Accept()
  2. 启动客户端,连接server,server阻塞在conn.Read(msg)
  3. 客户端发送数据,然后阻塞在conn.Read(buf[:]),server接收数据
  4. server发送数据,客户端接收,双方退出

可以看到tcp是有连接的,Conn代表了一次连接,下面是其接口定义: 包含了读写、关闭、查看地址、设置超时时间等。

type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

为了体现出tcp双工通信的优势,下面利用go并发编程,开N个客户端,对每一个客户端,server开一个gorouting处理请求,代码如下:

func Test_tcp_server2(t *testing.T) {
	listen, _ := net.Listen("tcp", "127.0.0.1:8848")
	defer listen.Close()
	num := 0
	for {
		conn, err := listen.Accept()
		fmt.Println(num)
		num++
		if err != nil {
			fmt.Println(err)
			return
		}
		go func(conn net.Conn) {
			fmt.Println(conn.LocalAddr(), conn.RemoteAddr())

			msg := make([]byte, 128)
			n, _ := conn.Read(msg)
			fmt.Println(string(msg[:n]))

			conn.Write([]byte("get it!"))

		}(conn)

	}
}

func Test_tcp_client2(t *testing.T) {
	var wg sync.WaitGroup
	start := time.Now()
	wg.Add(100)
	for i := 0; i < 100; i++ {
		go func(n int) {
			conn, err := net.Dial("tcp", "127.0.0.1:8848")
			if err != nil {
				fmt.Println(err)
				return
			}
			msg := "hello, my id is " + strconv.Itoa(n)
			conn.Write([]byte(msg)) // 发送数据
			buf := [512]byte{}
			num, _ := conn.Read(buf[:])
			fmt.Println(n, string(buf[:num]))
			err = conn.Close() // 关闭连接
			wg.Done()
			if err != nil {
				fmt.Println(err)
				return
			}

		}(i)
	}
	wg.Wait()
	fmt.Println(time.Since(start).String())
}

执行结果:

=== RUN   Test_tcp_client2
8 get it!
0 get it!
9 get it!
2 get it!
4 get it!
3 get it!
6 get it!
7 get it!
5 get it!
1 get it!
2.7159ms
--- PASS: Test_tcp_client2 (0.00s)

还有要注意的是TCP是流式数据,接收者可以分次接收,发送者也可以分次发送。

UDP

udp就比tcp简单很多,因为没有连接状态的确认,发送也不保证顺序和确认可达。go实现如下:

func Test_udp_Server1(t *testing.T) {
	listen, _ := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 8848,
	})

	defer listen.Close()

	var data [1024]byte
	n, addr, _ := listen.ReadFromUDP(data[:]) // 接收数据
	fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
	listen.WriteToUDP(data[:n], addr) // 发送数据
}

func Test_udp_Client1(t *testing.T) {
	socket, _ := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 8848,
	})

	defer socket.Close()
	sendData := []byte("Hello server")
	socket.Write(sendData) // 发送数据

	data := make([]byte, 4096)
	n, remoteAddr, _ := socket.ReadFromUDP(data) // 接收数据

	fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}

运行结果:

=== RUN   Test_udp_Server1
data:Hello server addr:127.0.0.1:55415 count:12
--- PASS: Test_udp_Server1 (2.86s)
=== RUN   Test_udp_Client1
recv:Hello server addr:127.0.0.1:8848 count:12
--- PASS: Test_udp_Client1 (0.00s)

可以看到,udp在收到消息时,会获得客户端的套接字,就可以对客户端返回消息。

总结

  • tcp是有状态的连接,可以实现数据流传输,可以分次写入、读取数据。
  • udp是无状态的,不能实现流式传输,必须一次性读取所有数据