美文网首页
简单的Socket图解,附Python和C#用例代码,以及双向同

简单的Socket图解,附Python和C#用例代码,以及双向同

作者: KwokKwok | 来源:发表于2018-01-12 20:08 被阅读2025次

    写这篇文章主要是因为自己以前并不怎么用Socket,在面对Socket时会总想要回避,不明觉厉。但后来仔细想想其实它很好理解,但是靠一堆术语讲一个概念很容易让人头蒙。所以我想写篇文章记录下自己的理解。另外网上的Socket实例很多都是阻塞式通信(单方向或固定顺序,比如很多聊天小程序),这里我也针对双向同时通信写了一些示例代码。

    内容摘要

    1. Socket简介
    2. 代码实例
      1. C#实例
      2. Python实例
      3. 双向同时通信(Python实例)

    内容开始

    网络可以理解为连接、连好了就传数据,这其中有一对协议来确定怎么建立连接、怎么保证数据传输的完整性等。Socket是这样一个东西,你告诉它,你要连谁,然后连接成功了就可以收发数据了,不用关心协议的具体实现。所以说它是对网络通信中一堆协议的封装,让你基本不用考虑底层实现就能轻松实现网络通信。

    TCP/UDP和Socket的关系

    TCP/UDP是真正的通信方式,Socket是对他们的封装。什么意思呢,

    1. TCP的面向连接和UDP的面向无连接什么意思?
      1. 可以把TCP理解为打电话,UDP理解为写信。TCP需要先建立连接,确保对方在线,才能进行通信。而UDP则是你把对方的地址写好,发出去就行了,收到收不到也不用管(基于UDP协议做优化不在本文讨论范围内)。
      2. 使用Socket时的区别:
        • 发送信息:TCP必须连接成功后才能发送(电话接通后你说话才有意义),UDP直接发送就好了(寄信知道地址就行了)
        • 接收信息:TCP必须绑定IP和端口号监听连入,然后建立连接(接电话)。UDP只要绑定了IP和端口号就行(房子在就能收到信)
    2. TCP的面向流连接和UDP的面向报文
      1. 流,可以理解为数据是源源不断的到达。报文,可以理解为数据是一次到达的。这个东西可以结合上面的面向连接和面向无连接理解。
      2. 与Socket的关系
        1. Socket构建的时候都需要指定传输方式。主要就是这两种,流传输格式(TCP)和数据报格式(UDP)。
        2. 传输限制。单次传输限制都存在,但流的方式可以将大块数据分割,进行多次传输,然后再将内容拼接起来,就好像数据没有被分割一样,表现就是使用流传输(TCP)的时候我们一般不需要考虑传输限制。而数据报的话就需要考虑单次传输大小限制了,这个根据不同的Socket实现也有所不同。
        3. 传输顺序。TCP面向连接和流传输的方式可以保证数据到达的先后顺序。而UDP面向无连接和报文传输的方式无法保证数据到达的先后顺序,甚至到不到达都无法保证。
    3. 常用的其实就是基于TCP的Socket,也就是在构建Socket的时候指定使用流传输方式。而UDP除了需要注意传输限制,使用起来要简单很多。下面我们也主要分析TCP这种方式。

    基于TCP的Socket的使用流程

    Socket这个东西的使用,在我看来,很像我们平时打客服电话。比如我们打10086转人工,我们谁都可以打这
    个号码,然后10086会给我们分配一个客服来进行真正的交流。

    对各种编程语言来说,也都是一样的流程。并且网络其实可以说是作为一种硬件资源使用的,可以看作是对端口的读写,所以你只要两边的协议一致(传输协议比如TCP、传输方式比如流、字符编码比如UTF-8),理论上就可以正常通信。它是和语言无关的,你用Python写服务端,用Java写客户端完全没有问题,想一下移动端用的推送服务就是这样。下面我们就来简单分析一下。

    使用流程

    1. 服务端启动一个监听用的Socket,可以称为listener(listener=10086)
    2. listener不断的监听有没有客户端来连接自己,等待连接,对应accept()方法(10086等着客户拨打这个号码)
    3. 一旦有可用连接连入(client),listener.accept()就会返回一个Socket实例,可以称为clientExecutor,这个就类似和你交流的客服(你拨打了10086,10086做出反应,给你分配了一个客服)
    4. 客户端client和服务端的clientExecutor可以进行通信了(你和客服可以交流了),这里需要注意的问题是,两边的编码要一致。

    流程图如下:


    Socket使用流程

    通信分析

    连接建立之后,就可以开始通信了。其实现可以简单理解为下面的方式:

    1. 可以认为调用完成send(),数据已经发送到对方的缓存中了。
    2. 调用receive()从己方缓冲读数据。
    3. 关于同时双向通信
      1. 如果使用的是TCP的方式,因为TCP是全双工的,可以同时双向传输。
      2. 如果使用的是UDP的方式,因为UDP是无连接的,甚至可以同时一对多,多对多传输,所以也就没有相关的限制。
    通信示意图

    代码实例

    下面我们写一些实例代码,并看一下效果。

    需求分析

    1. 客户端在连接成功的时候,会收到服务端发送的欢迎消息。(服务端发消息给客户端)
    2. 然后客户端可以给服务端发送消息。(客户端发消息给服务端)
    3. 服务端对来自不同客户端的消息做出反应(这里就直接将消息和消息来源打印出来,实际也可以根据这些信息做特殊处理)。

    Python实现

    服务端
    import socket
    import threading
    import time
    
    # 当新的客户端连入时会调用这个方法
    def on_new_connection(client_executor, addr):
        print('Accept new connection from %s:%s...' % addr)
    
        # 发送一个欢迎信息
        client_executor.send(bytes('Welcome'.encode('utf-8')))
    
        # 进入死循环,读取客户端发送的信息。
        while True:
            msg = client_executor.recv(1024).decode('utf-8')
            if(msg == 'exit'):
                print('%s:%s request close' % addr)
                break
            print('%s:%s: %s' % (addr[0], addr[1], msg))
        client_executor.close()
        print('Connection from %s:%s closed.' % addr)
    
    # 构建Socket实例、设置端口号和监听队列大小
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.bind(('192.168.5.103', 9999))
    listener.listen(5)
    print('Waiting for connect...')
    
    # 进入死循环,等待新的客户端连入。一旦有客户端连入,就分配一个线程去做专门处理。然后自己继续等待。
    while True:
        client_executor, addr = listener.accept()
        t = threading.Thread(target=on_new_connection, args=(client_executor, addr))
        t.start()
    
    客户端
    import socket
    
    # 构建一个实例,去连接服务端的监听端口。
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('192.168.5.103', 9999))
    
    # 接收欢迎信息
    msg=client.recv(1024)
    print('New message from server: %s' % msg.decode('utf-8'))
    
    # 不断获取输入,并发送给服务端。
    data=""
    while(data!='exit'):
        data=input()
        client.send(data.encode('utf-8'))
    client.close()
    
    效果
    Python效果

    C#实现

    服务端
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Text;
    
    namespace ServerSocket
    {
        class Program
        {
            static void Main(string[] args)
            {
                // 构建Socket实例、设置端口号和监听队列大小
                var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                string host = "192.168.5.103";
                int port = 9999;
                listener.Bind(new IPEndPoint(IPAddress.Parse(host), port));
                listener.Listen(5);
                Console.WriteLine("Waiting for connect...");
    
                // 进入死循环,等待新的客户端连入。一旦有客户端连入,就分配一个Task去做专门处理。然后自己继续等待。
                while(true){
                    var clientExecutor=listener.Accept();
                    Task.Factory.StartNew(()=>{
                        // 获取客户端信息,C#对(ip+端口号)进行了封装。
                        var remote=clientExecutor.RemoteEndPoint;
                        Console.WriteLine("Accept new connection from {0}",remote);
    
                        // 发送一个欢迎消息
                        clientExecutor.Send(Encoding.UTF32.GetBytes("Welcome"));
    
                        // 进入死循环,读取客户端发送的信息
                        var bytes=new byte[1024];
                        while(true){
                            var count=clientExecutor.Receive(bytes);
                            var msg=Encoding.UTF32.GetString(bytes,0,count);
                            if(msg=="exit"){
                                System.Console.WriteLine("{0} request close",remote);
                                break;
                            }
                            Console.WriteLine("{0}: {1}",remote,msg);
                            Array.Clear(bytes,0,count);
                        }
                        clientExecutor.Close();
                        System.Console.WriteLine("{0} closed",remote);
                    });
                }
            }
        }
    }
    
    客户端
    using System;
    using System.Threading;
    using System.Text;
    using System.Net;
    using System.Net.Sockets;
    
    namespace ClientSocket
    {
        class Program
        {
            static void Main(string[] args)
            {
                var host="192.168.5.103";
                var port=9999;
    
                // 构建一个Socket实例,并连接指定的服务端。这里需要使用IPEndPoint类(ip和端口号的封装)
                Socket client=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
    
                try
                {
                    client.Connect(new IPEndPoint(IPAddress.Parse(host),port));
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                    return;
                }
    
                // 接受欢迎信息
                var bytes=new byte[1024];
                var count=client.Receive(bytes);
                Console.WriteLine("New message from server: {0}",Encoding.UTF32.GetString(bytes,0,count));
    
                // 不断的获取输入,发送给服务端
                var input="";
                while(input!="exit"){
                    input=Console.ReadLine();
                    client.Send(Encoding.UTF32.GetBytes(input));
                }
    
                client.Close();
            }
        }
    }
    
    效果
    C#效果

    Python与C#互联

    消息编码不一致

    能发消息,但是解码会出现问题。(此处C#方的编码是UTF32,Python方是UTF-8)

    消息编码不一致
    消息编码一致

    消息正常收发。

    消息编码一致

    双向自由通信示例(使用Python)

    这里旨在验证是否可以同时收发信息。

    因为不能让同一个终端即接受输入又不断输出,所以将之前的Python代码稍作改动,做以下规定:

    1. 终端只接受输入,发送消息。
    2. 收到消息后写到文件里。
    服务端
    import socket
    import threading
    import time
    
    # 当新的客户端连入时会调用这个方法
    def on_new_connection(client_executor, addr):
        print('Accept new connection from %s:%s...' % addr)
    
        # 启动一个线程进入死循环,不断接收消息。
        recy_thread=threading.Thread(target=message_receiver, args=(client_executor,addr))
        recy_thread.start()
    
        # 不断获取输入,并发送给服务端。
        data=""
        while(data!='exit'):
            data=input()
            client_executor.send(data.encode('utf-8'))
        client_executor.close()
        print('Connection from %s:%s closed.' % addr)
    
    # 接收数据的线程需要处理的逻辑
    def message_receiver(client_executor,addr):
        while True:
            with open('server.txt','a+') as f:
                msg = client_executor.recv(1024).decode('utf-8')
                f.writelines('%s:%s: %s \r\n' % (addr[0], addr[1], msg))
    
    # 构建Socket实例、设置端口号和监听队列大小
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.bind(('192.168.5.103', 9999))
    listener.listen(5)
    print('Waiting for connect...')
    
    # 进入死循环,等待新的客户端连入。一旦有客户端连入,就分配一个线程去做专门处理。然后自己继续等待。
    while True:
        client_executor, addr = listener.accept()
        t = threading.Thread(target=on_new_connection, args=(client_executor, addr))
        t.start()
    
    客户端
    import socket
    import threading
    
    # 接收数据的线程逻辑
    def message_receiver(client):
        while True:
            with open('client.txt','a+') as f:
                msg = client.recv(1024).decode('utf-8')
                f.writelines('%s: %s \r\n' % ('来自服务端的消息', msg))
    
    # 构建一个实例,去连接服务端的监听端口。
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('192.168.5.103', 9999))
    
    # 启动线程专门用于接收数据
    recy_thread=threading.Thread(target=message_receiver, args=(client,))
    recy_thread.start()
    
    # 不断获取输入,并发送给服务端。
    data=""
    while(data!='exit'):
        data=input()
        client.send(data.encode('utf-8'))
    client.close()
    
    效果
    Python自由通信
    双向自由通信总结

    其实就是用双方都用了两个线程来处理,一个线程负责发送,一个线程负责接收。Python如此,其他语言也是如此。

    在真实使用场景中:

    1. 发送可以是手动调用而不是等待终端的输入,接收到数据后做些处理而不是简单的读到文件中。
    2. 需要线程同步的地方要注意。
    3. 接收:一般需要使用线程阻塞式接收。发送:如果不是很频繁的话,需要发送的时候异步执行一下即可。

    相关文章

      网友评论

          本文标题:简单的Socket图解,附Python和C#用例代码,以及双向同

          本文链接:https://www.haomeiwen.com/subject/lbfqoxtx.html