【Golang】快速複習指南QuickReview(九)——socket

Socket網路編程對於B/S項目來說,幾乎不會涉及;但是如果涉及遊戲服務器開發,或者上位機服務器開發,自定義通信協議,Socket網絡編程就變得常見了。

Socket編程

1.C#的socket

  • 1.創建Socket對象,指定傳輸層協議TCP或者UDP - Socket
//創建一個負責監聽IP地址跟端口號的Socket
serverSocket = new Socket(AddressFamily.InterNetwork,
                          SocketType.Stream,
                          ProtocolType.Tcp);
  • 2.綁定端口 - Bind()
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 9999));
  • 3.監聽
serverSocket.Listen();
  • 4.阻塞,等待客戶端連接 - Accept
Socket client = serverSocket.Accept();
  • 5.客戶端連接 - Connect() 與發送信息 - Send()
clientSocket.Connect(ip, port);
  • 6.服務端解除阻塞,接收消息 - Receive()
byte[] msg = new byte[1024 * 1024 * 2];
int msgLen = client.Receive(msg);

後面便是週而復始的,接收、發送的戲份。

在整個過程中,有以下步驟需要多線程處理:

  • Accept():由於服務端Accept()操作會阻塞線程,所以需要多線程,使其每接收一個客戶端連接,就開一個線程進行獨立處理。
  • Receive():由於Receive()操作也會阻塞線程,所以也需要開啓線程,才能進行與客戶端或服務器的交互操作。

1.1 服務端

class Program
{
    static void Main(string[] args)
    {
        Server server = new Server();
        server.Start();
        Console.ReadKey();
    }
}

public class Server
{
    private Socket serverSocket;
    //泛型集合 或者 字典
    private List<Socket> clientList;
    Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();

    public Server()
    {
        //創建一個負責監聽IP地址跟端口號的Socket
        serverSocket = new Socket(AddressFamily.InterNetwork,
                                  SocketType.Stream,
                                  ProtocolType.Tcp);

        clientList = new List<Socket>();
    }

    public void Start()
    {
        //監聽 telnet 192.168.11.78 9999
        serverSocket.Bind(new IPEndPoint(IPAddress.Any, 9999));

        serverSocket.Listen(10);
        Console.WriteLine("Server Start...");

        Thread threadAccept = new Thread(Accept);
        threadAccept.IsBackground = true;
        threadAccept.Start();
    }

    /// <summary>
    /// serverSocket可以作爲參數  object
    /// </summary>
    private void Accept()
    {
        //等待客戶端的連接,會掛起當前線程(如果是winfrom wpf 主線程裏使用這個方法會卡死) 接收客戶端請求,併爲之創建通信的socket---負責通信
        Socket client = serverSocket.Accept();//等待連接 所以要開啓線程

        //拿到遠程客戶端的IP地址和端口號
        IPEndPoint clientDetail = client.RemoteEndPoint as IPEndPoint;
        Console.WriteLine($"ip{clientDetail.Address} {clientDetail.Port} connecting");

        //存儲客戶端List
        clientList.Add(client);
        dicSocket.Add(clientDetail.ToString(), client);

        Thread receiveAccept = new Thread(Receive);
        receiveAccept.IsBackground = true;
        receiveAccept.Start(client);

        //按順序執行,尾遞歸便於理解  假死,一個客戶端
        Accept();//如果有一個連接了  就會依次執行 接收好客戶端後的處理,所以要加上一個尾遞歸

        ////或者使用循環
        //while (true)
        //{
        //    //上面所有的代碼,排除尾遞歸
        //}
    }


    public void Receive(object obj)
    {
        Socket client = obj as Socket;
        IPEndPoint clientDetail = client.RemoteEndPoint as IPEndPoint;

        try
        {
            byte[] msg = new byte[1024 * 1024 * 2];

            //實際接收到到的有效字節數 遠程客戶端一關  就接收不到 msgLen=0
            int msgLen = client.Receive(msg);

            Console.WriteLine($"ip{clientDetail.Address} {clientDetail.Port} say:{Encoding.UTF8.GetString(msg, 0, msgLen)}");

            if (msgLen != 0)
            {
                //client.Send(Encoding.UTF8.GetBytes("樓上說的對"));
                //改造後
                Broadcast(client, $"服務器時間{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")},ip{clientDetail.Address} {clientDetail.Port} say:{Encoding.UTF8.GetString(msg, 0, msgLen)}");

                //尾遞歸 不停的接收客戶端消息 同上可使用循環
                Receive(client);
            }
        }
        catch
        {
            Console.WriteLine($"ip{clientDetail.Address} {clientDetail.Port} 斷開");
            clientList.Remove(client);
        }
    }

    private void Broadcast(Socket socketOther, string msg)
    {
        //遍歷客戶端
        foreach (var client in clientList)
        {
            if (client != socketOther)
            {
                client.Send(Encoding.UTF8.GetBytes(msg));
            }
        }
    }
}

1.2 客戶端

class Program
{
    static void Main(string[] args)
    {
        Client client = new Client();
        client.Connect("127.0.0.1", 9999);

        Console.WriteLine("請輸入聊天內容,輸入quit退出:");
        string msg = Console.ReadLine();

        while (msg != "quit")
        {
            client.Send(msg);
            msg = Console.ReadLine();
        }
        Console.ReadKey();
    }
}

public class Client
{
    private Socket clientSocket;

    public Client()
    {
        //創建負責通信的socket
        this.clientSocket = new Socket(AddressFamily.InterNetwork,
                                       SocketType.Stream,
                                       ProtocolType.Tcp);
    }


    public void Connect(string ip, int port)
    {
        clientSocket.Connect(ip, port);
        Console.WriteLine("Client connect success...");

        Thread receiveAccept = new Thread(Receive);
        receiveAccept.IsBackground = true;
        receiveAccept.Start();
        //Receive();
    }


    private void Receive()
    {
        try
        {
            byte[] msg = new byte[1024*1024*2];
            int msgLen = clientSocket.Receive(msg);
            Console.WriteLine($"Server say:{Encoding.UTF8.GetString(msg, 0, msgLen)} ");
        }
        catch (Exception)
        {
            Console.WriteLine("服務器斷開");
        }
        Receive();
    }

    public void Send(string msg)
    {
        clientSocket.Send(Encoding.UTF8.GetBytes(msg));
    }
}

2.Golang的socket

2.1 服務端

Golang創建服務端省略了些步驟,直接從監聽Listen開始,博主開始把goroutine作線程類比C#的寫法,也是沒問題的。後面參考了包中的示例

ln, err := net.Listen("tcp", ":8080")
if err != nil {
    // handle error
}
for {
    conn, err := ln.Accept()
    if err != nil {
        // handle error
    }
    go handleConnection(conn)
}

利用死循環、goroutineAccept()返回多個值。可以有效簡化代碼。

package main

import (
	"bufio"
	// "encoding/binary"
	// "encoding/json"
	"fmt"
	"net"
)

func handleConnection(conn net.Conn) {
	defer conn.Close() // 關閉連接
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		n, err := reader.Read(buf[:]) // 讀取數據
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到client端發來的數據:", recvStr)
		conn.Write([]byte(recvStr)) // 發送數據
	}
}

func main() {
	
	listen, err := net.Listen("tcp", "127.0.0.1:9999")
	if err != nil {
		// handle error
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		conn, err := listen.Accept() // 建立連接
		if err != nil {
			// handle error
			fmt.Println("accept failed, err:", err)
			continue
		}
		go handleConnection(conn)
	}
}

22 客戶端

客戶端方面有一點不同,net包裏有單獨方法Dial(),大概翻譯了一下叫: 撥號

package main

import (
	"bufio"
	// "encoding/binary"
	// "encoding/json"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:9999")
	if err != nil {
		fmt.Println("err :", err)
		return
	}
	defer conn.Close() // 關閉連接
	inputReader := bufio.NewReader(os.Stdin)
	fmt.Println("請輸入內容,按回車發送,按q鍵退出...")
	for {
		//讀取輸入流,直到換行符出現爲止
		input, _ := inputReader.ReadString('\n') // 讀取用戶輸入

		//截取回車與換行
		inputInfo := strings.Trim(input, "\r\n")

		if strings.ToUpper(inputInfo) == "Q" { // 如果輸入q就退出
			return
		}
		_, err = conn.Write([]byte(inputInfo)) // 發送數據
		if err != nil {
            fmt.Println(err)
			return
		}
		buf := [512]byte{}
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("recv failed, err:", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}
}

再次強調:這個系列並不是教程,如果想系統的學習,博主可推薦學習資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章