上一篇文章介绍了golang进行UDP编程的方式,本篇文章继续golang的网络编程,我们看看如何实现TCP编程。UDP传输的是数据包,传输时不会建立实际的连接,UDP传输数据不会保证可靠性。TCP则不同,它会维持客户端和服务器之间的连接,并且保证数据传输的可靠性,服务器和客户端之间会维护连接,使用流的方式进行数据传输。因此,UDP客户端接收的是一个个数据包,而TCP客户端接收到的是流,因此会存在数据粘包的问题,关于数据粘包问题的解决方案,网络上相关的文章很多,大家可以了解一下。
UDP服务端只需要监听主机的IP和端口即可,TCP的服务端会经历Listen,Accept,之后才可以通过连接进行通信,Listen阶段,服务器监听主机的IP和端口,而Accept服务器阻塞等待客户端与之进行连接,当客户端与服务器经过三次握手建立连接后,就可以基于Accept返回的连接进行通信了。关于Socket网络编程的基本原理,可以看看之前Java的文章中的理论部分。
服务器的配置如下
package config
const (
SERVER_IP = "127.0.0.1"
SERVER_PORT = 10006
SERVER_RECV_LEN = 10
)
看看服务端的代码
package main
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"go-study/socket/config"
)
func main() {
address := config.SERVER_IP + ":" + strconv.Itoa(config.SERVER_PORT)
listener, err := net.Listen("tcp", address)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
continue
}
defer conn.Close()
for {
data := make([]byte, config.SERVER_RECV_LEN)
_, err = conn.Read(data)
if err != nil {
fmt.Println(err)
break
}
strData := string(data)
fmt.Println("Received:", strData)
upper := strings.ToUpper(strData)
_, err = conn.Write([]byte(upper))
if err != nil {
fmt.Println(err)
break
}
fmt.Println("Send:", upper)
}
}
}
前面一部分是Listen和Accept,当Accept返回conn连接后,我们在一个for循环中处理客户端的请求,逻辑和UDP的类似,读取数据,转换大写,之后写出数据。
客户端的代码,除了使用TCP外,和UDP的情况基本相同
package main
import (
"bufio"
"fmt"
"net"
"os"
"strconv"
"go-study/socket/config"
)
func checkError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
address := config.SERVER_IP + ":" + strconv.Itoa(config.SERVER_PORT)
conn, err := net.Dial("tcp", address)
checkError(err)
defer conn.Close()
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
lineLen := len(line)
n := 0
for written := 0; written < lineLen; written += n {
var toWrite string
if lineLen-written > config.SERVER_RECV_LEN {
toWrite = line[written : written+config.SERVER_RECV_LEN]
} else {
toWrite = line[written:]
}
n, err = conn.Write([]byte(toWrite))
checkError(err)
fmt.Println("Write:", toWrite)
msg := make([]byte, config.SERVER_RECV_LEN)
n, err = conn.Read(msg)
checkError(err)
fmt.Println("Response:", string(msg))
}
}
}
首先通过Dial建立与服务器的连接,之后读取标标准输入的行,将其传递给服务器,然后从服务器读取响应。这里处理粘包采用了自己封包的方式。
上面的服务器程序存在一个问题,那就是一次只能为一个客户端提供服务(for循环),如果一个客户端连接时间过长,可能会让服务器无法服务于其他的客户端,这就是拒绝服务攻击,最简单的解决这种问题的方式就是将conn的处理单独启动线程,在golang中我们很幸运,启动一个协程即可。
package main
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"go-study/socket/config"
)
func main() {
address := config.SERVER_IP + ":" + strconv.Itoa(config.SERVER_PORT)
listener, err := net.Listen("tcp", address)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
for {
data := make([]byte, config.SERVER_RECV_LEN)
_, err := conn.Read(data)
if err != nil {
fmt.Println(err)
break
}
strData := string(data)
fmt.Println("Received:", strData)
upper := strings.ToUpper(strData)
_, err = conn.Write([]byte(upper))
if err != nil {
fmt.Println(err)
break
}
fmt.Println("Send:", upper)
}
}
上面的代码,我们将客户端数据处理的部分封装了一个handleConn函数,在与客户端连接建立时,通过golang的go关键字调用handleConn函数,从而启动了协程进行处理,这样,main函数会立刻重新阻塞在Accept上,从而保证服务器可以时刻监听客户端连接的建立,避免了拒绝防御攻击。