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

2018-01-12  本文已影响2025人  KwokKwok

写这篇文章主要是因为自己以前并不怎么用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. 接收:一般需要使用线程阻塞式接收。发送:如果不是很频繁的话,需要发送的时候异步执行一下即可。
上一篇下一篇

猜你喜欢

热点阅读