写这篇文章主要是因为自己以前并不怎么用Socket,在面对Socket时会总想要回避,不明觉厉。但后来仔细想想其实它很好理解,但是靠一堆术语讲一个概念很容易让人头蒙。所以我想写篇文章记录下自己的理解。另外网上的Socket实例很多都是阻塞式通信(单方向或固定顺序,比如很多聊天小程序),这里我也针对双向同时通信写了一些示例代码。
内容摘要
- Socket简介
- 代码实例
- C#实例
- Python实例
- 双向同时通信(Python实例)
内容开始
网络可以理解为连接、连好了就传数据,这其中有一对协议来确定怎么建立连接、怎么保证数据传输的完整性等。Socket是这样一个东西,你告诉它,你要连谁,然后连接成功了就可以收发数据了,不用关心协议的具体实现。所以说它是对网络通信中一堆协议的封装,让你基本不用考虑底层实现就能轻松实现网络通信。
TCP/UDP和Socket的关系
TCP/UDP是真正的通信方式,Socket是对他们的封装。什么意思呢,
- TCP的面向连接和UDP的面向无连接什么意思?
- 可以把TCP理解为打电话,UDP理解为写信。TCP需要先建立连接,确保对方在线,才能进行通信。而UDP则是你把对方的地址写好,发出去就行了,收到收不到也不用管(基于UDP协议做优化不在本文讨论范围内)。
- 使用Socket时的区别:
- 发送信息:TCP必须连接成功后才能发送(电话接通后你说话才有意义),UDP直接发送就好了(寄信知道地址就行了)
- 接收信息:TCP必须绑定IP和端口号监听连入,然后建立连接(接电话)。UDP只要绑定了IP和端口号就行(房子在就能收到信)
- TCP的面向流连接和UDP的面向报文
- 流,可以理解为数据是源源不断的到达。报文,可以理解为数据是一次到达的。这个东西可以结合上面的面向连接和面向无连接理解。
- 与Socket的关系
- Socket构建的时候都需要指定传输方式。主要就是这两种,流传输格式(TCP)和数据报格式(UDP)。
- 传输限制。单次传输限制都存在,但流的方式可以将大块数据分割,进行多次传输,然后再将内容拼接起来,就好像数据没有被分割一样,表现就是使用流传输(TCP)的时候我们一般不需要考虑传输限制。而数据报的话就需要考虑单次传输大小限制了,这个根据不同的Socket实现也有所不同。
- 传输顺序。TCP面向连接和流传输的方式可以保证数据到达的先后顺序。而UDP面向无连接和报文传输的方式无法保证数据到达的先后顺序,甚至到不到达都无法保证。
- 常用的其实就是基于TCP的Socket,也就是在构建Socket的时候指定使用流传输方式。而UDP除了需要注意传输限制,使用起来要简单很多。下面我们也主要分析TCP这种方式。
基于TCP的Socket的使用流程
Socket这个东西的使用,在我看来,很像我们平时打客服电话。比如我们打10086转人工,我们谁都可以打这
个号码,然后10086会给我们分配一个客服来进行真正的交流。
对各种编程语言来说,也都是一样的流程。并且网络其实可以说是作为一种硬件资源使用的,可以看作是对端口的读写,所以你只要两边的协议一致(传输协议比如TCP、传输方式比如流、字符编码比如UTF-8),理论上就可以正常通信。它是和语言无关的,你用Python写服务端,用Java写客户端完全没有问题,想一下移动端用的推送服务就是这样。下面我们就来简单分析一下。
使用流程
- 服务端启动一个监听用的Socket,可以称为
listener
(listener=10086) -
listener
不断的监听有没有客户端来连接自己,等待连接,对应accept()
方法(10086等着客户拨打这个号码) - 一旦有可用连接连入(
client
),listener.accept()
就会返回一个Socket实例,可以称为clientExecutor
,这个就类似和你交流的客服(你拨打了10086,10086做出反应,给你分配了一个客服) - 客户端
client
和服务端的clientExecutor
可以进行通信了(你和客服可以交流了),这里需要注意的问题是,两边的编码要一致。
流程图如下:
通信分析
连接建立之后,就可以开始通信了。其实现可以简单理解为下面的方式:
- 可以认为调用完成
send()
,数据已经发送到对方的缓存中了。 - 调用
receive()
从己方缓冲读数据。 - 关于同时双向通信
- 如果使用的是TCP的方式,因为TCP是全双工的,可以同时双向传输。
- 如果使用的是UDP的方式,因为UDP是无连接的,甚至可以同时一对多,多对多传输,所以也就没有相关的限制。
代码实例
下面我们写一些实例代码,并看一下效果。
需求分析
- 客户端在连接成功的时候,会收到服务端发送的欢迎消息。(服务端发消息给客户端)
- 然后客户端可以给服务端发送消息。(客户端发消息给服务端)
- 服务端对来自不同客户端的消息做出反应(这里就直接将消息和消息来源打印出来,实际也可以根据这些信息做特殊处理)。
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()
效果
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();
}
}
}
效果
Python与C#互联
消息编码不一致
能发消息,但是解码会出现问题。(此处C#方的编码是UTF32,Python方是UTF-8)
消息编码一致
消息正常收发。
双向自由通信示例(使用Python)
这里旨在验证是否可以同时收发信息。
因为不能让同一个终端即接受输入又不断输出,所以将之前的Python代码稍作改动,做以下规定:
- 终端只接受输入,发送消息。
- 收到消息后写到文件里。
服务端
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如此,其他语言也是如此。
在真实使用场景中:
- 发送可以是手动调用而不是等待终端的输入,接收到数据后做些处理而不是简单的读到文件中。
- 需要线程同步的地方要注意。
- 接收:一般需要使用线程阻塞式接收。发送:如果不是很频繁的话,需要发送的时候异步执行一下即可。