精华内容
下载资源
问答
  • java socket通信自定义消息协议

    热门讨论 2010-05-26 16:26:34
    java socket通信自定义消息协议,socket以字节码的方式通信传递,客户端与服务器端分别进行转换与解析的过程实现简单的消息协议
  • JAVA自定义网络通信协议

    千次阅读 2013-05-11 18:51:23
     JAVA默认提供了对file,ftp,gopher,http,https,jar,mailto,netdoc协议的支持。当我们要利用这些协议来创建应用时,主要会涉及到如下几个类:  1.java.net.URL:URL资源  2.java.net.URLConnection:各种URL资源...

     

           JAVA默认提供了对file,ftp,gopher,http,https,jar,mailto,netdoc协议的支持。当我们要利用这些协议来创建应用时,主要会涉及到如下几个类:

          1.java.net.URL:URL资源

          2.java.net.URLConnection:各种URL资源连接器

        例如,当我们利用HTTP协议获取Web资源时,通常的过程如下:

     

    URL url = new URL("http://www.163.com");
    URLConnection conneciotn = url.openConnection();

     

     

            URL和URLConnection是如何做到对协议支持的呢?在它们的内部,主要涉及到了如下几个类:

            1.URLStreamHandler:协议的流处理器,负责对特定URL协议的解析,并创建符合当前协议的URLConnection;

            2.URLStreamHandlerFactory:协议处理工厂,负责为特定协议找到正确的URLStreamHandler。

            当利用URL对象创建资源时,其构造函数在去掉协议字段后将其传给URLStreamHandlerFactory,由该工厂来接受协议,为该协议找到并创建适当的URLStreamHandler实现类,最后存储在URL对象的一个字段中(即URL类中transient修饰的URLStreamHandler成员属性)。
           URLStreamHandler和URLConnection总是成对出现的。因此,若要实现对新协议的支持时,需同时实现这两个抽象类,分别负责对协议的解析,以及与服务器的交互(数据转换等)。

          另外,JAVA是如何识别当前URL协议该由哪个URLStreamHandler和URLConnection来处理的呢?在创建URL对象时,其内部调用了一个getURLStreamHandler(String protocol)静态方法,它将根据协议的名称来找到对应的URLStreamHandler实现类,其查找规则如下:

         1)检测是否创建了URLStreamHandlerFactory对象:如果创建,则直接使用createURLStreamHandler(String protocol)方法创建的协议处理器,否则进入步骤2);

         2)在java.protocol.handler.pkgs系统属性指定的包中查找与协议同名的子包和名为Handler的类,即负责处理当前协议的URLStreamHandler实现类必须在<包名>.<协议名定义的包>中,并且类名称必须为Handler。例如:com.company.net.protocol.rdp包中的Handler类将用于处理RDP协议。若仍未找到则进入步骤3);

        3)在JDK rt.jar中的sun.net.www.protocol.<name>包中查找Handler类。例如,若当前协议为ftp,则URL所使用协议处理器就应该为sun.net.www.protocol.ftp包中的Handler类。如下图:
             下面结合一个实例来说明如何开发一个新的网络协议。

              背景:senv(Server Environment Protocol)协议可让客户端连接远程服务器后发送出请求,请求的内容就是URL查询参数部分,该协议的默认端口为9527。例如:senv://192.168.1.101:9527?pro=os.name,java.version表示获取192.168.1.101这台主机的操作系统名和JRE的版本信息,如果没有查询参数,客户端将默认发送一个"?",表示获取所有的系统属性信息。

          
     1.Senv协议处理器

    package com.daniele.appdemo.net.protocol.custom.senv;
    
    import java.io.IOException;
    import java.net.URL;
    import java.net.URLConnection;
    import java.net.URLStreamHandler;
    
    /**
     * <p>
     * 		自定义的senv协议处理器。
     *      由于senv协议的格式符合标准的URL格式:
     *      	protocol://username@hostname:port/path/filename?query#fragment
     *      因此,这个实现类只需实现父类中的openConnection()方法即可。否则,需重写父类方法
     *      protected void parseURL(URL u, String spec, int start, int limit),
     *      来重新正确的设置URL的各个属性值,例如:host,port,query等。
     * </p> 
     * @author  <a href="mailto:code727@gmail.com">Daniele</a>
     * @version 1.0.0, 2013-5-8
     * @see     
     * @since   AppDemo1.0.0
     */
    public class Handler extends URLStreamHandler {
    
    	/**
    	 * <p>当URL根据协议找到该处理器并调用openConnection()方法后,返回负责处理该协议连接的连接器</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @param u
    	 * @return
    	 * @throws IOException 
    	 * @since AppDemo1.0.0
    	 */
    	@Override
    	protected URLConnection openConnection(URL u) throws IOException {
    		return new SenvURLConnection(u);
    	}
    	
    }

     2.Senv协议连接器

    package com.daniele.appdemo.net.protocol.custom.senv;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.net.URL;
    import java.net.URLConnection;
    
    import com.daniele.appdemo.util.StringUtils;
    
    /**
     * <p>自定义senv协议连接器</p> 
     * @author  <a href="mailto:code727@gmail.com">Daniele</a>
     * @version 1.0.0, 2013-5-8
     * @see     
     * @since   AppDemo1.0.0
     */
    public class SenvURLConnection extends URLConnection {
    	
    	/** senv协议的默认端口号 */
    	public static final int DEFAULT_PORT = 9527;
    	
    	private Socket connection = null;
    
    	public SenvURLConnection(URL url) {
    		super(url);
    	}
    	
    	/**
    	 * <p>由于父类URLConnection中的getInputStream()方法不提供输入流的获取实现逻辑,因此这里需要重写此方法</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @return
    	 * @throws IOException 
    	 * @since AppDemo1.0.0 
    	 */
    	@Override
    	public synchronized InputStream getInputStream() throws IOException {
    		if (!connected)
    			this.connect();
    		return connection.getInputStream();
    	}
    	
    	/**
    	 * <p>senv协议连接操作</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @throws IOException 
    	 * @since AppDemo1.0.0 
    	 */
    	@Override
    	public synchronized void connect() throws IOException {
    		if (!connected) {
    			int port = url.getPort();
    			if (port < 1 || port > 65535)
    				port = DEFAULT_PORT;
    			this.connection = new Socket(url.getHost(), port);
    			connected = true;
    			// 连接后立即发送请求
    			sendRequest(url);
    		}
    	}
    	
    	/**
    	 * <p>发送senv协议请求</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @param u
    	 * @throws IOException 
    	 * @since AppDemo1.0.0
    	 */
    	protected void sendRequest(URL u) throws IOException {
    		OutputStream outputStream = this.connection.getOutputStream();
    		
    		String queryString = u.getQuery();
    		
    		/*
    		 *  将URL的查询参数部分发送给服务器,由服务器负责解析查询后返回结果。
    		 *  当参数参数部分为空时,则发送一个"?",表示查询服务器系统环境的所有信息。
    		 */
    		outputStream.write(StringUtils.isNotNullOrBlank(queryString)? queryString.getBytes() : "?".getBytes());
    		outputStream.flush();
    	}
    
    }

     

    3.协议处理器工厂

    package com.daniele.appdemo.net.protocol.factory;
    
    import java.net.URLStreamHandler;
    import java.net.URLStreamHandlerFactory;
    
    /**
     * <p>
     *       自定义协议的处理器工厂,负责针对每种自定义的协议而返回它们各自对应的协议处理器
     *       如果要用上述的查找规则1来安装协议处理器时,则需要用到这个类
     *</p> 
     * @author  <a href="mailto:code727@gmail.com">Daniele</a>
     * @version 1.0.0, 2013-5-9
     * @see     
     * @since   AppDemo1.0.0
     */
    public class CustomProtocolFactory implements URLStreamHandlerFactory {
    
    	public URLStreamHandler createURLStreamHandler(String protocol) {
    		if ("senv".equalsIgnoreCase(protocol))
    			return new com.daniele.appdemo.net.protocol.custom.senv.Handler();
    		return null;
    	}
    
    }
    

     

    4.处理Senv协议的服务器

    package com.daniele.appdemo.net.protocol.test;
    
    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    import java.util.Iterator;
    
    import org.apache.log4j.Logger;
    
    import com.daniele.appdemo.util.StringUtils;
    import com.daniele.appdemo.util.SystemUtils;
    
    /**
     * <p>处理Senv协议的服务器
     *    1.接收客户端请求
     *	  2.发送响应结果
     *  </p> 
     * @author  <a href="mailto:code727@gmail.com">Daniele</a>
     * @version 1.0.0, 2013-5-10
     * @see     
     * @since   AppDemo1.0.0
     */
    
    public class SenvProtocolServer {
    	
    	private static final Logger logger = Logger.getLogger(SenvProtocolServer.class);
    	
    	/** Senv协议的请求参数标识 */
    	public static final String REQUEST_PARAM_MARK = "pro=";
    	
    	/** Senv协议服务的默认端口号 */
    	private static final int DEFAULT_PORT = 9527;
    
    	/** 服务器的IP或主机名 */
    	private String host;
    	
    	/** 绑定了Senv协议服务的端口号 */
    	private int port = 9527;
    	
    	/** 当前就绪的服务端通道 */
    	private ServerSocketChannel serverChannel;
    	
    	/** 当前就绪的客户端通道 */
    	private SocketChannel clientChannel;
    	
    	/** 服务端的事件注册器 */
    	private Selector selector;
    	
    	/**
    	 * <p>启动Senv协议服务器</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @throws IOException 
    	 * @since AppDemo1.0.0
    	 */
    	public void start() throws IOException {
    		
    		serverChannel = ServerSocketChannel.open();
    		
    		if (port < 1 || port > 65535)
    			port = DEFAULT_PORT;
    		
    		if (StringUtils.isNotNullOrBlank(host)) {
    			serverChannel.socket().bind(new InetSocketAddress(InetAddress.getByName(host), port));
    			logger.info("Start server " + host + ":" + port);
    		} else {
    			serverChannel.socket().bind(new InetSocketAddress(port));
    			logger.info("Start server on port " + port);
    		}
    		serverChannel.configureBlocking(false);
    		selector = Selector.open();
            serverChannel.register(selector, SelectionKey.OP_ACCEPT); 
            handle();
    	}
    	
    	/**
    	 * <p>处理Senv协议请求</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @throws IOException 
    	 * @since AppDemo1.0.0
    	 */
    	protected void handle() throws IOException {
    		while (true) {
    			selector.select();
    			Iterator<SelectionKey> keySetIterator = selector.selectedKeys().iterator();
    			SelectionKey cuurentKey = null;
    			while (keySetIterator.hasNext()) {
    				// 获取当前就绪通道的键对象
    				cuurentKey = keySetIterator.next();
    				// 避免同一个就绪通道被重复处理
    				keySetIterator.remove();
    				try {
    					if (cuurentKey.isAcceptable()) {
    						serverChannel = (ServerSocketChannel) cuurentKey.channel();
    						clientChannel = serverChannel.accept();
    						if (clientChannel != null) {
    							logger.info("Receive request from " 
    									+ clientChannel.socket().getInetAddress().getHostAddress() + ":"
    									+ clientChannel.socket().getLocalPort());
    							clientChannel.configureBlocking(false);
    							clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
    						} 
    					} else {
    						clientChannel = (SocketChannel) cuurentKey.channel();
    						if (cuurentKey.isReadable()) 
    							writeResponse();
    					}
    				} catch (IOException e) {
    					if (clientChannel != null && clientChannel.isOpen())
    						try {
    							/*
    							 *  为防止服务端在读写客户端信息时,客户端由于某种原因被意外关闭引起服务端也被强制关闭的情况发生。
    							 *  需在catch块中也需要对客户端的通道做关闭处理, 从而防止服务端也被强制关闭的严重问题。
    							 *  另外,对就绪通道的读写过程需单独的在一个try...catch块中。
    							 */
    							clientChannel.close();
    						} catch (IOException ioe) {
    							ioe.printStackTrace();
    						}
    				} 
    			}
    		}
    	}
    	
    	/**
    	 * <p>读取客户端请求</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @return 
    	 * @throws IOException 
    	 * @throws  
    	 * @since AppDemo1.0.0
    	 */
    	protected String readRequest() throws IOException {
    		StringBuffer request = new StringBuffer();
    		CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
    		ByteBuffer buffer = ByteBuffer.allocate(1024);
    		while (clientChannel.read(buffer) > 0) {
    			buffer.flip();
    			request.append(decoder.decode(buffer).toString());
    			buffer.clear();
    		}
    		return request.toString();
    	}
    	
    	/**
    	 * <p>向客户端返回响应结果</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @throws IOException 
    	 * @since AppDemo1.0.0
    	 */
    	protected void writeResponse() throws IOException {
    		String request = readRequest();
    		int start = -1;
    		// 如果发送的请求为"?"或请求中无指定的参数时,则查询所有的系统环境属性
    		if ("?".equals(request) || 
    				(start = request.toLowerCase().indexOf(REQUEST_PARAM_MARK)) < 0) {
    			clientChannel.write(ByteBuffer.wrap(SystemUtils.formatSystemProperties().getBytes()));
    		} else {
    			// 获取请求参数值
    			String queryValueString = request.substring(start + REQUEST_PARAM_MARK.length());
    			if (StringUtils.isNullOrBlank(queryValueString))
    				clientChannel.write(ByteBuffer.wrap(SystemUtils.formatSystemProperties().getBytes()));
    			else {
    				int index = queryValueString.indexOf("&");
    				if (index > -1)
    					/*
    					 *  如果请求参数值里出现了"&"字符,
    					 *  则说明这个字符后面的内容则认为是其它一些请求参数的内容,
    					 *  因此不对这部分内容作处理
    					 */
    					queryValueString = queryValueString.substring(0, index);
    				clientChannel.write(ByteBuffer.wrap(SystemUtils.formatSystemProperties(queryValueString.split(",")).getBytes()));
    			}
    		}
    		/*
    		 *  响应内容被发送出去之后添加换行标识,
    		 *  目的是让客户端的BufferedReader对象调用readLine()方法后能将当前行的内容读取出来
    		 */
    		clientChannel.write(ByteBuffer.wrap("\n".getBytes()));
    		
    		/*
    		 *  发送完响应信息后马上关闭与客户端之间的通道。
    		 *  目的在于让客户端读取完这些响应之后,就立即释放掉资源,从而让读操作不会一直处于阻塞状态
    		 */
    		clientChannel.close();
    	}
    	public String getHost() {
    		return host;
    	}
    
    	public void setHost(String host) {
    		this.host = host;
    	}
    
    	public int getPort() {
    		return port;
    	}
    
    	public void setPort(int port) {
    		this.port = port;
    	}
    
    	public static void main(String[] args) {
    		SenvProtocolServer server = new SenvProtocolServer();
    		server.setHost("192.168.1.101");
    		try {
    			server.start();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    
    }

     

     4.Senv协议请求客户端

    /**
     * <p>
     * 	  Senv协议请求的客户端,主要功能分为:
     *    1.在创建第一次创建URL对象之前,添加对自定义协议的支持
     *    2.发送请求
     *    3.展示响应数据
     * </p> 
     * @author  <a href="mailto:code727@gmail.com">Daniele</a>
     * @version 1.0.0, 2013-5-9
     * @see     
     * @since   AppDemo1.0.0
     */
    
    public class SenvProtocolClient {
    	
    	public static void main(String[] args) {
    		BufferedReader reader = null;
    		try {
    			// 配置协议处理器查找规则一
    			if (StringUtils.isNullOrBlank(System.getProperty("java.protocol.handler.pkgs"))) {
    				// 设置各个协议包所在的父包路径
    				System.setProperty("java.protocol.handler.pkgs", "com.daniele.appdemo.net.protocol.custom");
    			}
    			/*
                             * 配置协议处理器查找规则二
                             * 这种方式在整个应用范围之内只能被执行一次。
                             * 如果多于一次则会出现"java.lang.Error: factory already defined"这样的错误。但不会受规则一的限制.
                             */ 
    //			URL.setURLStreamHandlerFactory(new CustomProtocolFactory());
    			
    			URL url = new URL("senv://192.168.1.101:9527/");
    			reader = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream()));
    			String result = "";
    			while ((result = reader.readLine()) != null)
    				System.out.println(result);
    		} catch (IOException e) {
    			e.printStackTrace();
    		} finally {
    			try {
    				if (reader != null)
    					reader.close();
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    }

     

    package com.daniele.appdemo.util;
    
    import java.util.Enumeration;
    import java.util.Properties;
    
    /**
     * <p>运行环境工具类</p> 
     * @author  <a href="mailto:code727@gmail.com">Daniele</a>
     * @version 1.0.0, 2013-5-9
     * @see     
     * @since   AppDemo1.0.0
     */
    
    public class SystemUtils {
    	
    	private static Properties properties = null;
    	
    	static {
    		properties = System.getProperties();
    	}
    	
    	/**
    	 * <p>返回格式化后的所有系统属性信息</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @return 
    	 * @since AppDemo1.0.0
    	 */
    	@SuppressWarnings("unchecked")
    	public static String formatSystemProperties() {
    		StringBuffer formatResult = new StringBuffer();
    		Enumeration<String> names = (Enumeration<String>) properties.propertyNames();
    		while (names.hasMoreElements()) {
    			String name = names.nextElement();
    			formatResult.append(name).append("=")
    				.append(properties.getProperty(name)).append("\n");
    		}
    		int length = 0;
    		return (length = formatResult.length()) > 0 ? 
    				formatResult.substring(0, length - 1) : "";
    	}
    	
    	/**
    	 * <p>返回格式化后的所有指定的系统属性信息</p> 
    	 * @author <a href="mailto:code727@gmail.com">Daniele</a> 
    	 * @param propertyKeys
    	 * @return 
    	 * @since AppDemo1.0.0
    	 */
    	public static String formatSystemProperties(String[] propertyKeys) {
    		StringBuffer formatResult = new StringBuffer();
    		if (propertyKeys != null && propertyKeys.length > 0) {
    			for (String key : propertyKeys) 
    				formatResult.append(key).append("=")
    					.append(properties.getProperty(key)).append("\n");
    		}
    		int length = 0;
    		return (length = formatResult.length()) > 0 ? 
    				formatResult.substring(0, length - 1) : "";
    	}
    
    }
    package com.daniele.appdemo.util;
    
    public class StringUtils {
    	
    	public static boolean isNullOrBlank(String str) {
    		return str == null || str.length() == 0;
    	}
    	
    	public static boolean isNotNullOrBlank(String str) {
    		return !isNullOrBlank(str);
    	}
    
    }

    运行时,依次启动SenvProtocolServer和SenvProtocolClient类即可。

     

    展开全文
  • 自定义通信协议

    千次阅读 2018-01-08 09:48:59
    1.自定义数据通信协议  这里所说的数据协议是建立在物理层之上的通信数据包格式。所谓通信的物理层就是指我们通常所用到的RS232、RS485、红外、光纤、无线等等通信方式。在这个层面上,底层软件提供两个基本的操

    现在大部分的仪器设备都要求能过通过上位机软件来操作,这样方便调试,利于操作。其中就涉及到通信的过程。在实际制作的几个设备中,笔者总结出了通信程序的通用写法,包括上位机端和下位机端等。

    1.自定义数据通信协议

      这里所说的数据协议是建立在物理层之上的通信数据包格式。所谓通信的物理层就是指我们通常所用到的RS232、RS485、红外、光纤、无线等等通信方式。在这个层面上,底层软件提供两个基本的操作函数:发送一个字节数据、接收一个字节数据。所有的数据协议全部建立在这两个操作方法之上。

    通信中的数据往往以数据包的形式进行传送的,我们把这样的一个数据包称作为一帧数据。类似于网络通信中的TCPIP协议一般,比较可靠的通信协议往往包含有以下几个组成部分:帧头、地址信息、数据类型、数据长度、数据块、校验码、帧尾

      帧头和帧尾用于数据包完整性的判别,通常选择一定长度的固定字节组成,要求是在整个数据链中判别数据包的误码率越低越好。减小固定字节数据的匹配机会,也就是说使帧头和帧尾的特征字节在整个数据链中能够匹配的机会最小。通常有两种做法,一、减小特征字节的匹配几率。二、增加特征字节的长度。通常选取第一种方法的情况是整个数据链路中的数据不具有随即性,数据可预测,可以通过人为选择帧头和帧尾的特征字来避开,从而减小特征字节的匹配几率。使用第二种方法的情况更加通用,适合于数据随即的场合。通过增加特征字节的长度减小匹配几率,虽然不能够完全的避免匹配的情况,但可以使匹配几率大大减小,如果碰到匹配的情况也可以由校验码来进行检测,因此这种情况在绝大多说情况下比较可靠。

      地址信息主要用于多机通信中,通过地址信息的不同来识别不同的通信终端。在一对多的通信系统中,可以只包含目的地址信息。同时包含源地址和目的地址则适用于多对多的通信系统。 

      数据类型、数据长度和数据块是主要的数据部分。数据类型可以标识后面紧接着的是命令还是数据。数据长度用于指示有效数据的个数。

      校验码则用来检验数据的完整性和正确性。通常对数据类型、数据长度和数据块三个部分进行相关的运算得到。最简单的做法可是对数据段作累加和,复杂的也可以对数据进行CRC运算等等,可以根据运算速度、容错度等要求来选取。

     

    2.上位机和下位机中的数据发送

      物理通信层中提供了两个基本的操作函数,发送一个字节数据则为数据发送的基础。数据包的发送即把数据包中的左右字节按照顺序一个一个的发送数据而已。当然发送的方法也有不同。

      在单片机系统中,比较常用的方法是直接调用串口发送单个字节数据的函数。这种方法的缺点是需要处理器在发送过程中全程参与,优点是所要发送的数据能够立即的出现在通信线路上,能够立即被接收端接收到。另外一种方法是采用中断发送的方式,所有需要发送的数据被送入一个缓冲区,利用发送中断将缓冲区中的数据发送出去。这种方法的优点是占用处理器资源小,但是可能出现需要发送的数据不能立即被发送的情况,不过这种时延相当的小。对于51系列单片机,比较倾向于采用直接发送的方式,采用中断发送的方式比较占用RAM资源,而且对比直接发送来说也没有太多的优点。以下是51系列单片机中发送单个字节的函数。

    void SendByte(unsigned char ch)

    {

         SBUF = ch;

         while(TI == 0);

         TI = 0;

    }

      上位机中关于串口通信的方式也有多种,这种方式不是指数据有没有缓冲的问题,而是操作串口的方式不同,因为PC上数据发送基本上都会被缓冲后再发送。对于编程来说操作串口有三种方式,一、使用windows系统中自带的串口通信控件,这种方式使用起来比较简单,需要注意的是接收时的阻塞处理和线程机制。二、使用系统的API直接进行串口数据的读取,在windows和linux系统中,设备被虚拟为文件,只需要利用系统提供的API函数即可进行串口数据的发送和读取。三、使用串口类进行串口操作。在此只介绍windows环境下利用串口类编程的方式。

      CSerialPort是比较好用的串口类。它提供如下的串口操作方法:

      void WriteToPort(char* string, int len);

      串口初始化成功后,调用此函数即可向串口发送数据。为了避免串口缓冲所带来的延时,可以开启串口的冲刷机制。

     

    3.下位机中的数据接收和协议解析

      下位机接收数据也有两种方式,一、等待接收,处理器一直查询串口状态,来判断是否接收到数据。二、中断接收。两种方法的优缺点在此前的一篇关于串口通信的文章中详细讨论过。得出的结论是采用中断接收的方法比较好。

      数据包的解析过程可以设置到不同的位置。如果协议比较简单,整个系统只是处理一些简单的命令,那么可以直接把数据包的解析过程放入到中断处理函数中,当收到正确的数据包的时候,置位相应的标志,在主程序中再对命令进行处理。如果协议稍微复杂,比较好的方式是将接收的数据存放于缓冲区中,主程序读取数据后进行解析。也有两种方式交叉使用的,比如一对多的系统中,首先在接收中断中解析“连接”命令,连接命令接收到后主程序进入设置状态,采用查询的方式来解析其余的协议。

      以下给出具体的实例。在这个系统中,串口的命令非常简单。所有的协议全部在串口中断中进行。数据包的格式如下:

      0x55, 0xAA, 0x7E, 0x12, 0xF0, 0x02, 0x23, 0x45, SUM, XOR, 0x0D

      其中0x55, 0xAA, 0x7E为数据帧的帧头,0x0D为帧尾,0x12为设备的目的地址,0xF0为源地址,0x02为数据长度,后面接着两个数据0x23, 0x45,从目的地址开始结算累加、异或校验和,到数据的最后一位结束。

            协议解析的目的,首先判断数据包的完整性,正确性,然后提取数据类型,数据等数据,存放起来用于主程序处理。代码如下:

     

    if(state_machine == 0)     // 协议解析状态机

    {

        if(rcvdat == 0x55)     // 接收到帧头第一个数据

            state_machine = 1;

        else

            state_machine = 0;    // 状态机复位

    }

    else if(state_machine == 1)

    {

        if(rcvdat == 0xAA)     // 接收到帧头第二个数据

            state_machine = 2;

        else

            state_machine = 0;    // 状态机复位

    }

    else if(state_machine == 2)

    {

        if(rcvdat == 0x7E)     // 接收到帧头第三个数据

            state_machine = 3;

         else

            state_machine = 0;    // 状态机复位

    }

    else if(state_machine == 3)

    {

        sumchkm = rcvdat;     // 开始计算累加、异或校验和

        xorchkm = rcvdat;

        if(rcvdat == m_SrcAdr)    // 判断目的地址是否正确

            state_machine = 4;

        else

            state_machine = 0;

    }

    else if(state_machine == 4)

    {

        sumchkm += rcvdat;

        xorchkm ^= rcvdat;

        if(rcvdat == m_DstAdr)    // 判断源地址是否正确

            state_machine = 5;

        else

            state_machine = 0;

       }

    else if(state_machine == 5)

    {

        lencnt = 0;        // 接收数据计数器

        rcvcount = rcvdat;      // 接收数据长度

        sumchkm += rcvdat;

        xorchkm ^= rcvdat;

        state_machine = 6;

    }

    else if(state _machine == 6 || state _machine == 7)

    {

        m_ucData[lencnt++] = rcvdat;     // 数据保存

        sumchkm += rcvdat;

        xorchkm ^= rcvdat;

        if(lencnt == rcvcount)    // 判断数据是否接收完毕

            state_machine = 8;

        else

            state_machine = 7;

    }

    else if(state_machine == 8)

    {

        if(sumchkm == rcvdat)    // 判断累加和是否相等

            state_machine = 9;

        else

            state_machine = 0;

    }

    else if(state_machine == 9)

    {

        if(xorchkm == rcvdat)    // 判断异或校验和是否相等

            state_machine = 10;

        else

            state_machine = 0;

    }

    else if(state_machine == 10)

    {

        if(0x0D == rcvdat)     // 判断是否接收到帧尾结束符

        {

            retval = 0xaa;    // 置标志,表示一个数据包接收到

        }

        state_machine = 0;     // 复位状态机

    }

     

      此过程中,使用了一个变量state_machine作为协议状态机的转换状态,用于确定当前字节处于一帧数据中的那个部位,同时在接收过程中自动对接收数据进行校验和处理,在数据包接收完的同时也进行了校验的比较。因此当帧尾结束符接收到的时候,则表示一帧数据已经接收完毕,并且通过了校验,关键数据也保存到了缓冲去中。主程序即可通过retval的标志位来进行协议的解析处理。

      接收过程中,只要哪一步收到的数据不是预期值,则直接将状态机复位,用于下一帧数据的判断,因此系统出现状态死锁的情况非常少,系统比较稳定,如果出现丢失数据包的情况也可由上位机进行命令的补发,不过这种情况笔者还没有碰到。

      对于主程序中进行协议处理的过程与此类似,主程序循环中不断的读取串口缓冲区的数据,此数据即参与到主循环中的协议处理过程中,代码与上面所述完全一样。

     

    4.上位机中的数据接收和命令处理 

      上位机中数据接收的过程与下位机可以做到完全一致,不过针对不同的串口操作方法有所不同。对于阻赛式的串口读函数,例如直接进行API操作或者调用windows的串口通信控件,最好能够开启一个线程专门用于监视串口的数据接收,每接收到一个数据可以向系统发送一个消息。笔者常用的CSerialPort类中就是这样的处理过程。CSerialPort打开串口后开启线程监视串口的数据接收,将接收的数据保存到缓冲区,并向父进程发送接收数据的消息,数据将随消息一起发送到父进程。父进程中开启此消息的处理函数,从中获取串口数据后就可以把以上的代码拷贝过来使用。 

      CSerialPort向父类发送的消息号如下:

      #define WM_COMM_RXCHAR WM_USER+7 // A character was received and placed in the input buffer.

      因此需要手动添加此消息的响应函数:

      afx_msg LONG OnCommunication(WPARAM ch, LPARAM port);

      ON_MESSAGE(WM_COMM_RXCHAR, OnCommunication)

      响应函数的具体代码如下:

    LONG CWellInfoView::OnCommunication(WPARAM ch, LPARAM port)

    {

         int retval = 0;

         rcvdat = (BYTE)ch;

         if(state_machine == 0)     // 协议解析状态机

        {

          if(rcvdat == 0x55)     // 接收到帧头第一个数据

              state_machine = 1;

          else

              state_machine = 0;    // 状态机复位

        }

        else if(state_machine == 1)

        {

          if(rcvdat == 0xAA)     // 接收到帧头第二个数据

              state_machine = 2;

          else

              state_machine = 0;    // 状态机复位

        ......

     

    5.总结

      以上给出的是通信系统运作的基本雏形,虽然简单,但是可行。实际的通信系统中协议比这个要复杂,而且涉及到数据包响应、命令错误、延时等等一系列的问题,在这样的一个基础上可以克服这些困难并且实现出较为稳定可靠的系统

    展开全文
  • TCP自定义通信协议

    千次阅读 2018-04-12 07:29:19
    回答:因为我们在传输的过程中会出现分包(没有接受到一个完整的包)与黏包(收到比一包多的数据,除去完整的一包,首尾有数据)的问题,所以我这里我是服务端自定义通信协议,客户端协议解析。由于我这里是要实现一...

    Tcp 服务端自定义协议,客户端解析协议的处理

    • 我们想想,为什么要定义协议:
    • 回答:因为我们在传输的过程中会出现分包(没有接受到一个完整的包)与黏包(收到比一包多的数据,除去完整的一包,首尾有数据)的问题,所以我这里我是服务端自定义通信协议,客户端协议解析。由于我这里是要实现一个下位机给我发数据,我要将他的数据拿到并以图的形式显示在app界面。

    • 接下来我们看下面,协议是参考这个的,图片是网上找的,代码中的同步字我定义的是一字节,帧类型是一字节,信息长度是2字节,数据是2字节,结束字2字节(这个出了点下问题在测试)

    • 图片链接和图片上传

    • 先来看看服务端代码:

    import com.google.common.primitives.Bytes;
    import com.google.common.primitives.Shorts;
    
    import java.io.BufferedInputStream;
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Scanner;
    
    /**
     * @Author Wangjj
     * @Create 2018/4/8.
     * @Content
     */
    public class Server {
    
        private static void genProtocol(DataOutputStream out, String msg) throws IOException {
            // 数据
            byte[] data = msg.getBytes();
            List<Byte> sendList = new ArrayList<Byte>();
            // 协议头
            byte head = 0xaa - 256;
            sendList.add(head);
            //帧类型
            byte type = 0x30;
            sendList.add(type);
            //数据长度
            short len = (short) data.length;
            byte[] lenArr = Shorts.toByteArray(len);
            for (byte l : lenArr) {
                sendList.add(l);
            }
            for (byte d : data) {
                sendList.add(d);
            }
    //        //结束字
    //        byte foot = 0x55;
    //        sendList.add(foot);
            byte[] send = Bytes.toArray(sendList);
    //        Shorts.fromByteArray()
            out.write(send);                      //写入消息内容
            out.flush();
        }
    
        private void start() {
            try {
                /**
                 * 基于TCP协议的Socket通信,实现用户登录,服务端
                 */
                ServerSocket serverSocket = new ServerSocket(12306);//1024-65535的某个端口
                System.out.println("等待连接......");
                Socket s = serverSocket.accept();
                OutputStream os = s.getOutputStream();
                DataOutputStream outs = new DataOutputStream(os);
    
    
    
                while (true) {
                    Scanner scaner = new Scanner(System.in);
                    genProtocol(outs, scaner.next());
    
                    System.out.println("带有协议头的数据发送成功......");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            new Server().start();
        }
    }
    
    • 先来看看客户端代码:
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.Socket;
    import java.net.UnknownHostException;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    /**
     * @Author Wangjj
     * @Create 2018/4/11.
     * @Content
     */
    public class MainActivity extends AppCompatActivity {
    
        public TextView editSend;
    
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initView();
            initClientSocket();
        }
    
        public void initView() {
    
            editSend = findViewById(R.id.sendMsg);
    
        }
    
    
        private void initClientSocket() {
            new Thread() {
                public void run() {
                    try {
                        Socket s = new Socket("10.1.1.251", 12306);
                        System.out.println("连接成功......");
                        InputStream is = s.getInputStream();
                        while (true) {
                            byte[] buffer = new byte[1024];
                            int len = is.read(buffer);
                            if (len == -1) continue;
                            if (buffer[0] != (0xaa - 256)) break;//判断头
                            if (buffer[1] != 0x30) break;//判断帧类型
                            short datalen = Shorts.fromBytes(buffer[2], buffer[3]);//数据信息
                            String sss = new String(buffer, 4, datalen);
    //                        if (buffer[len-datalen] != 0x55) break;//结束字
    
                            Message msg = new Message();
                            msg.what = 0x123;
                            msg.obj = sss;
                            myhandler.sendMessage(msg);
                        }
                        s.close();
                    } catch (UnknownHostException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    
    
        @SuppressLint("HandlerLeak")
        final Handler myhandler = new Handler() {
            public void handleMessage(Message msg) {
                if (msg.what == 0x123) {
                    System.out.println("--------------->" + msg.obj);
                    editSend.setText((String) msg.obj);
                }
            }
    
        };
    }
    
    • 同时,ble和usb通讯时,解析协议也可以这么处理。文章如若哪里有问题,希望大家指出来,我加以改正。

    展开全文
  • 上期,转载了一篇自定义通信协议的制定以及使用的一篇帖子,个人觉得相当不错。 但是就目前而已,谷歌的PB使用要更广泛一些,不管是哪个方面。 对于PB的话,也就是Google Protocol Buffer。假如您在网上搜索,...

    上期,转载了一篇自定义的通信协议的制定以及使用的一篇帖子,个人觉得相当不错。

    但是就目前而已,谷歌的PB使用要更广泛一些,不管是哪个方面。

    对于PB的话,也就是Google Protocol Buffer。假如您在网上搜索,应该会得到类似这样的文字介绍:

    Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。

    protocolbuffer(以下简称PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

    官方文档给出的是:

    a language-neutral, platform-neutral, extensible way of serializing structured data for use in communications protocols, data storage, and more.

    或许您和我一样,在第一次看完这些介绍后还是不明白 Protobuf 究竟是什么,那么我想一个简单的例子应该比较有助于理解它。

    安装 Google Protocol Buffer

    可以在http://download.csdn.net/download/canlets/6878023 下载protobuf2.5.0。

    安装步骤如下所示:

    tar zxvf protobuf-2.4.1.tar.gz
    cd protobuf-2.4.1
    ./configure
    make
    make check
    make install
    安装结束。

    验证:
    查看是否安装成功:protoc --version
    如果出现:libprotoc 2.4.1 则说明安装成功!

    如果出现错误:

    tar zxvf protobuf-2.5.0.tar.gz
    ./configure
    make
    make check
    make install
    安装结束。

    验证:
    查看是否安装成功:protoc --version
    如果出现:libprotoc 2.5.0 则说明安装成功!

    安装完成后在终端下执行

      vim ~/.profile (我加在了/etc/profile里)

      打开配置文件,在该文件中添加

      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

      然后保存退出,接下来执行

      source ~/.profile

      是配置文件修改生效,最后执行

      protoc --version

      查看protobuf版本以测试是否安装成功

    打开vsprojects目录,里面有一个.sln文件,打开 vsprojects里面有一个readme.txt,告诉了如何安装

    Protobuf的优点

    Protobuf 有如 XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

    它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。

    Protobuf 语义更清晰,无需类似 XML 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。

    使用 Protobuf 无需学习复杂的文档对象模型,Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf 比其他的技术更加有吸引力。


    Protobuf的缺点

    Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。

    由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容

    官方文档描述如下:for instance, protocol buffers would not be a good way to model a text-based document with markup (e.g. HTML), since you cannot easily interleave structure with text



    Protobuf环境的搭建

    步骤:

    1, 安装maven

    http://blog.csdn.net/jiangguilong2000/article/details/9284437

    2, 下载probuf源码和编译器

    http://blog.csdn.net/jiangguilong2000/article/details/9284297

    3, 拷贝文件,执行相应mvn install命令。 (protobuf并不提供jar包,需要自己执行命令生成)

    Note:

    在这个地方遇到了一个问题,错误信息如下:

    Failed to execute goal org.apache.maven.plugins:maven-antrun-plugin:1.3:run (generate-sources) on project protobuf-java: An Ant BuildException has occured: Execute failed: java.io.IOException: Cannot run program "..\src\protoc": CreateProcess error=2, ????????? ->

    原因:犯了了一个很2的问题,拷贝protoc.exe文件的路径拷错了。请详细比较步骤2中的路径,一定拷对路径。

    4,通过protoc.exe编译addressbook.proto文件

    http://blog.sina.com.cn/s/blog_653ac36d0101h9kn.html

    核心概念

    1,.proto文件

    相当于确定数据协议,数据结构中存在哪些数据,数据类型是怎么样

    2,modifiers

    2-1 required 不可以增加或删除的字段,必须初始化

    2-2 optional 可选字段,可删除,可以不初始化

    2-3 repeated 可重复字段, 对应到java文件里,生成的是List

    3,Message

    在proto文件里,数据的协议时以Message的形式表现的。

    4, Build

    生成具体的java类时,例如Person.java,同时会存在build方法。文档的意思是对于转化后的数据,具有唯一性,build提供了便利的方法来初始化这些数据。



    最后总结一下。人们一直在强调,同 XML 相比, Protobuf 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。

    对于这些 “小 3 到 10 倍”,“快 20 到 100 倍”的说法,严肃的程序员需要一个解释。因此在本文的最后,让我们稍微深入 Protobuf 的内部实现吧。

    有两项技术保证了采用 Protobuf 的程序能获得相对于 XML 极大的性能提高。

    第一点,我们可以考察 Protobuf 序列化后的信息内容。您可以看到 Protocol Buffer 信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。比如网络上传输的字节数更少,需要的 IO 更少等,从而提高性能。

    Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。

    第二点我们需要理解 Protobuf 封解包的大致过程,从而理解为什么会比 XML 快很多。

    首先我们来了解一下 XML 的封解包过程。XML 需要从文件中读取出字符串,再转换为 XML 文档对象结构模型。之后,再从 XML 文档对象结构模型中读取指定节点的字符串,最后再将这个字符串转换成指定类型的变量。这个过程非常复杂,其中将 XML 文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。

    反观 Protobuf,它只需要简单地将一个二进制序列,按照指定的格式读取到 C++ 对应的结构类型中就可以了。从上一节的描述可以看到消息的 decoding 过程也可以通过几个位移操作组成的表达式计算即可完成。速度非常快。

    所以综上所述,之前那篇的自定义的通信协议方法与PB有着非常相似的地方,但又不太相同。但是相对于xml而言,自定义的要优化了很多,但是比起PB却又有所不足,算是介于两者之间吧。但是由于PB最初是谷歌的内部方法,并且需要建立环境,相对比较麻烦,自定义的方法在这方面表现的就相当的简单,虽然xml也是如此,但是由于效率的缺乏,这点就显得非常的无力,虽然我们对xml的了解已经非常的深了。

    所以相比较之下。我个人认为,PB稍微优于自定义,明显优于xml,甚至可以淘汰xml了吧。




    展开全文
  • java 自定义通讯协议

    千次阅读 2017-07-12 17:58:09
    JAVA默认提供了对file,ftp,gopher,http,https,jar,mailto,netdoc协议的支持。当我们要利用这些协议来创建应用时,主要会涉及到如下几个类:  1.java.net.URL:URL资源  2.java.net.URLConnection:各种URL...
  • Java自定义URL协议

    2011-08-31 18:26:48
    Java 提供了对 ...等的实现,而当需要自己定义通信协议的时候,就需要利用 JDK 提供的对 URL 扩展机制进行自定义。   JDK 主要提供了如下三种方式对 URL 进行扩展,每种方式都有各自的使...
  • Netty实现自定义通信协议

    千次阅读 2019-03-10 11:39:56
    概述 在网络编程中,无论使用netty还是其它的socket通讯框架,都是通过TCP或UDP传输二进制流。发送方把要发送的对象转化成二进制流发送出去;接收方把接收到的二进制流转化为...自定义通信协议 在 easy-im 项目中...
  • 这里指的通信协议并不是狭义的TCP、UDP这类【标准通信协议】,而是指的Netty用于客户端与服务端之间数据交互的【自定义通信协议】。无论是使用 Netty 还是原始的 Socket 编程,基于 TCP 通信的数据包格式均为二进制...
  • Netty自定义通信协议(粘包拆包)

    千次阅读 2018-11-07 18:50:45
    Netty自定义通信协议主要是在解码器与编码器,其他的变动不大。netty入门实例:https://blog.csdn.net/zc_ad/article/details/83824911,此处将测试的demo共享出来,现在对netty没办法到用语言组织的程度,只能先将...
  • JAVA Socket通信中自定义TCP通信协议

    千次阅读 2015-06-29 00:27:20
    JAVA中Socket通信是以流的形式传输数据,对于InputStream和OutputStream的装饰问题,如果只是传输一串字符串,可以通过简单的BufferedReader和PrintWriter实现,但是在实际的客户端与服务端通信中不可能只是传递简单...
  • 一个简单的自定义通信协议(socket)
  • 自定义通信协议设计之TLV编码应用

    千次阅读 2016-12-08 18:06:48
    摘要: 在网络通信开发过程中,很可能需要使用到自定义通信协议,因为之前曾从事过电信信令类工作,接触较多的则是ASN.1中的BER、PER编码,其中BER是基于TLV方式进行编码,本文主要介绍一下TLV编码及其应用。...
  • java自定义协议

    2020-02-26 18:02:03
    在设计 C-S 结构的应用时,我们一般需要使用一种客户端C与服务端S的通信协议。现在常用的协议有HTTP,XML-RPC,SOAP等,当然,现在XML在很多场景下都用JSON替代了,这个不赘述,有些时候,我们需要设计一些自有协议,...
  • 一个简单的TCP自定义通信协议

    千次阅读 2018-06-22 15:23:52
    我们为了解决这些问题,需要我们自定义通信协议进行封包与解包。什么是分包与黏包?分包:指接受方没有接受到一个完整的包,只接受了部分。黏包:指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,...
  • 说明:在本SDK内,可自定义多种协议,以及专属的解析方式,如无特殊要求,则使用默认的协议 -- codvision 一、协议组织方式 协议是按照一定规范组织行程的字符串 本SDK采用协议形式为Uri形式 协议组织格式: [scheme...
  • void mySocket::sendMSG(QString msg, qint64 cmd) { if(!isValid()) //确保连接仍然有效 { qDebug()<<"losing connect......."; return;... /********************构造数据包************************/ ...
  • 看懂通信协议:自定义通信协议设计之TLV编码应用 TLV编解码Java实现 我的开源项目:一种TLV编解码器的实现 TLV 下面内容摘自: 看懂通信协议:自定义通信协议设计之TLV编码应用 因为之前从事过电信信令...
  • 一个简单的自定义通信协议 socket

    千次阅读 2018-11-05 12:38:55
    按照网络通信的传统,我们都会自定义协议,这有很多好处,大家可以自己体会(嘿嘿)。     一直不知道socket通信时候自定义数据包是什么样子的,偶然做了个小例子。   先来说说数据包的定义,我这里是包头+内容 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 62,656
精华内容 25,062
关键字:

java自定义通信协议

java 订阅