第十一章:网络编程
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。
因此可以用Socket来描述网络连接的一对一关系。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
1 TCP的C/S架构
1.1 单服务版本
服务端代码:
package main
import (
"fmt"
"net")
func main() {
// 创建监控
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
fmt.Println("listen err:", err)
return
}
defer listener.Close() // 主协程结束时,关闭listener
fmt.Println("服务器等待客户端建立连接...")
// 等待客户端连接请求
conn, err := listener.Accept()
if err != nil {
fmt.Println("accpet err:", err)
return
}
defer conn.Close() // 使用结束,断开与客户端链接
fmt.Println("客户端与服务器连接建立成功...")
// 接收客户端数据
buf := make([]byte, 1024) // 创建1024大小的缓冲区,用于read
n, err := conn.Read(buf) // 读取到n个大小的数据
if err != nil {
fmt.Println("read err:", err)
return
}
fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少
}
如图,在整个通信过程中,服务器端有两个socket参与进来,但用于通信的只有 conn 这个socket。它是由 listener创建的。隶属于服务器端。
客户端代码:
package main
import (
"fmt"
"net")
func main() {
// 主动发送连接请求
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
fmt.Println("Dial err", err)
}
defer conn.Close() // 结束时,关闭连接
// 发送数据
_, err = conn.Write([]byte("Are u ready?"))
if err != nil {
fmt.Println("Write err:", err)
return
}
}
1.2 并发服务
并发服务端:
Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。
需要注意的是,实现并发处理多个客户端数据的服务器,就需要针对每一个客户端连接,单独产生一个Socket,并创建一个单独的goroutine与之完成通信。
在判断客户端数据是否为“exit”字符串时,要注意,客户端会自动的多发送2个字符:$“\r\n”$(这在windows系统下代表回车、换行)
服务端代码:
package main
import (
"fmt"
"net" "strings")
func main() {
// 创建监控
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
fmt.Println("listen err:", err)
return
}
defer listener.Close() // 主协程结束时,关闭listener
for {
// 等待客户端连接请求
conn, err := listener.Accept()
if err != nil {
fmt.Println("accpet err:", err)
return
}
// 处理用户请求,新建一个协程
go HandleConn(conn)
}
}
// 处理用户请求
func HandleConn(conn net.Conn) {
// 函数调用完毕,自动关闭conn
defer conn.Close()
// 获取客户端发过来的网址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, "connect successful")
buf := make([]byte, 2048)
for {
// 读取用户数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err=", err)
return
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
fmt.Println("len = ", len(string(buf[:n])))
//if string(buf[:n-1]) == "exit" // nc测试,发送时,只有/n
if string(buf[:n-2]) == "exit" {
fmt.Println(addr, "exit")
return
}
// 将数据转化为大写,再给用户发送
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
并发客户端:
客户端不仅需要持续的向服务端发送数据,同时也要接收从服务端返回的数据。因此可将发送和接收放到不同的协程中。
主协程循环接收服务器回发的数据(该数据应已转换为大写),并打印至屏幕;子协程循环从键盘读取用户输入数据,写给服务器。读取键盘输入可使用 os.Stdin.Read(str)。定义切片str,将读到的数据保存至str中。
这样,客户端也实现了多任务。
package main
import (
"fmt"
"net" "os")
func main() {
// 主动发送连接请求
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
fmt.Println("Dial err", err)
}
defer conn.Close() // 客户端终止时,关闭于服务器通讯的socket
// 启动子协程: 接受用户键盘输入发送给服务端
go func() {
// 创建用于存储用户键盘输入数据的切片缓冲区
str := make([]byte, 1024)
for { // 反复读取
n, err := os.Stdin.Read(str) // 获取用户键盘输入(阻塞)
if err != nil {
fmt.Println("os.Stdin.Read err:", err)
return
}
// 从键盘读到的数据,发送给服务端
_, err = conn.Write(str[:n])
if err != nil {
fmt.Println("conn.Write err:", err)
return
}
}
}()
// 主协程: 接受服务端数据,进行打印输出
buf := make([]byte, 1024) // 定义用于存储服务器回发数据的切片缓冲区
for {
n, err := conn.Read(buf) // 从通信socket中读数据,存入切片缓冲区(阻塞)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
fmt.Printf("服务器回发: %s\n", string(buf[:n]))
}
}