在网络应用开发中,开发者首先要做的一个决定是使用 TCP 还是 UDP 作为传输层协议。TCP 是基于连接,并且基于字节流提供可靠的数据传输的协议。而 UDP 是无连接,通过数据包发送数据,并不保证送达的协议。
我们将在这一节分别利用 UDP 和 TCP 实现一套 client-server 程序。
该程序主要完成的功能是:
- client 从键盘读取一行字符串,并发给 server
- server 收到字符串并转换成大写
- server 将修改后的字符串发送给 client
- client 收到修改后的数据并显示
2.7.1 Socket Programming with UDP
UDP 在发送数据包时,需要先在数据包中附加地址的信息。网络会利用这个信息 route 数据包到达接收程序。
附加的地址信息应该包括:
- 目标 IP 地址
- 目标端口号
- 自身 IP 地址
- 自身端口号
附加自身 IP 地址和自身端口号一般不需要自己实现,操作系统会完成。
Golang 中对 UDP 地址的定义为:
type UDPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}
提供的接口中常用的地址包括 local 和 remote 的。
client
示例代码:
package udp
import (
"log"
"net"
)
func SendToServer(ip string, port string, content string) string {
remoteAddr, err := net.ResolveUDPAddr("udp", ip+":"+port)
if err != nil {
log.Printf("ResolveUDPAddr: %v\n", err)
return ""
}
conn, err := net.DialUDP("udp", nil, remoteAddr)
if err != nil {
log.Printf(
"Connect to %v failed: %v\n",
remoteAddr,
err)
return ""
}
defer conn.Close()
_, err = conn.Write([]byte(content))
if err != nil {
log.Fatal("WriteToUDP: ", err)
return ""
}
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
log.Printf(
"Receive from %v failed: %v\n",
remoteAddr,
err)
return ""
}
return string(buffer[:n])
}
- 调用
DialUDP
创建 UDP Socket 的文件描述符UDPConn
:
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
从接口可以看出,
这里需要注意,UDP 是无连接的,这里的 UDPConn
并非指一个“连接”,而是打开了一个 socket,并且设置好了源地址和目标地址。
- 调用
Write
方法向 socket 写入要发送给 server 的数据。
func (c *conn) Write(b []byte) (int, error)
- 调用
Read
读取来自 server 响应数据。
func (c *conn) Read(b []byte) (int, error)
server
示例代码:
package udp
import (
"log"
"net"
"strings"
)
func StartServer(port int) {
addr := net.UDPAddr {
Port: port,
IP: net.IP{127, 0, 0, 1},
}
l, err := net.ListenUDP("udp", &addr)
if err != nil {
log.Fatal("ListenUDP: ", err)
return
}
defer l.Close()
for {
data := make([]byte, 1024)
// Echo all incoming data.
n, remoteAddr, err := l.ReadFromUDP(data)
if err != nil {
log.Fatal("ReadFromUDP: ", err)
return
}
log.Printf("Received %d bytes from %v", n, remoteAddr)
s := strings.ToUpper(string(data[:n]))
data = []byte(s)
_, err = l.WriteToUDP(data, remoteAddr)
if err != nil {
log.Fatal("WriteToUDP: ", err)
return
}
log.Printf("Send %v to %v\n", s, remoteAddr)
}
}
- 调用
ListenUDP
开始监听
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
- (在
for
循环中)调用ReadFromUDP
阻塞式读取 client 发来的数据
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
- (在
for
循环中)调用WriteToUDP
将响应发送给 client
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
下面的图总结了上述过程。
- AF_INET 表示 IPv4
- SOCK_DGRAM 表示 UDP
- 无连接
2.7.2 Socket Programming with TCP
与 UDP 不同,TCP 是一个基于连接的协议。client 和 server 必须先建立连接才能发送数据。当 client 创建 TCP socket 时,他需要指定 server 的 IP 地址和端口号,然后进行三次握手建立 TCP 连接(对应用程序是透明的)。
Golang 中对 TCP 地址的定义为:
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}
我们发现这与 UDP 地址定义完全一致。
client
示例代码:
package tcp
import (
"log"
"net"
)
func SendToServer(ip string, port string, content string) string {
remoteAddr, err := net.ResolveTCPAddr("tcp", ip+":"+port)
if err != nil {
log.Printf("ResolveTCPAddr: %v\n", err)
return ""
}
conn, err := net.DialTCP("tcp", nil, remoteAddr)
if err != nil {
log.Printf(
"Connect to %v failed: %v\n",
remoteAddr,
err)
return ""
}
defer conn.Close()
_, err = conn.Write([]byte(content))
if err != nil {
log.Fatal("WriteToTCP: ", err)
return ""
}
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
log.Printf(
"Receive from %v failed: %v\n",
remoteAddr,
err)
return ""
}
return string(buffer[:n])
}
- 调用
DialTCP
创建 TCP 连接:
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
- 调用
Write
方法向 socket 写入要发送给 server 的数据。
func (c *conn) Write(b []byte) (int, error)
- 调用
Read
读取来自 server 的响应数据。
func (c *conn) Read(b []byte) (int, error)
可以看出,socket 读写并无区别,主要区别就是 DialTCP
和 DialUDP
。DialTCP
会创建 TCP 连接。
server
示例代码:
package tcp
import (
"log"
"net"
"strings"
)
func handleConnection(conn *net.TCPConn) {
defer conn.Close()
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
log.Fatal("ReadFromTCP: ", err)
return
}
log.Printf("Received %d bytes from %v", n, conn.RemoteAddr())
s := strings.ToUpper(string(buffer[:n]))
buffer = []byte(s)
_, err = conn.Write(buffer)
if err != nil {
log.Fatal("WriteToTCP: ", err)
return
}
log.Printf("Send %v to %v\n", s, conn.RemoteAddr())
}
func StartServer(port int) {
addr := net.TCPAddr {
Port: port,
IP: net.IP{127, 0, 0, 1},
}
l, err := net.ListenTCP("tcp", &addr)
if err != nil {
log.Fatal("ListenTCP: ", err)
return
}
defer l.Close()
for {
conn, err := l.AcceptTCP()
if err != nil {
log.Fatal("AcceptTCP: ", err)
return
}
go handleConnection(conn)
}
}
- 调用
ListenTCP
开始监听 TCP 连接请求
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
- (在
for
循环中)调用AcceptTCP
为 TCP 连接创建 socket
func (l *TCPListener) AcceptTCP() (*TCPConn, error)
- (在
for
循环中)调用Read
读取 client 发来的数据
func (c *conn) Read(b []byte) (int, error)
- (在
for
循环中)调用Write
方法向 socket 写入要发送给 client 的数据。
func (c *conn) Write(b []byte) (int, error)
需要注意的是 ListenTCP
和 AcceptTCP
的关系。这是 TCP 和 UDP 的关键区别。
如下图所示,ListenTCP
创建了一个 "Welcoming socket" 来专门接受 TCP 连接请求。AcceptTCP
会处理请求,并创建新的 socket 来与对应的 client 通信。因此它使用了两个 socket。而 UDP 则只使用了一个 socket。
在与 client 通信结束后,我们并不需要关闭 "Welcoming socket"。只需要关闭这个 client-server 之间的连接即可。
下面的图总结了上述过程。
参考文献
有疑问加站长微信联系(非本文作者)