golang TCP Socket编程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yjp19871013/article/details/82555417

上一篇文章介绍了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上,从而保证服务器可以时刻监听客户端连接的建立,避免了拒绝防御攻击。

猜你喜欢

转载自blog.csdn.net/yjp19871013/article/details/82555417