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]))
	}
}

再次强调:这个系列并不是教程,如果想系统的学习,博主可推荐学习资源。