开启掘金成长之旅!这是我参与「掘金日新计划 · 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)
执行流程:
- 启动server,监听端口,阻塞在listen.Accept()
- 启动客户端,连接server,server阻塞在conn.Read(msg)
- 客户端发送数据,然后阻塞在conn.Read(buf[:]),server接收数据
- 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是无状态的,不能实现流式传输,必须一次性读取所有数据