Java:Socket网络编程

一、“粘包”问题

  • 【粘包】是一个不存在的问题。
    这是因为开发者没设计好传输应用数据时的数据结构,依赖于socket的缓存边界作为数据包的边界,实际上这根本不可靠或者不正确,需要你在传输层上的数据流给出数据包边界,并且重新解析传输层的数据流来分出应用所需的数据包。
  • tcp协议是基于流的,属于传输层,仅负责发送数据,tcp并不关心应用逻辑层如何处理这些数据。所以在发送数据前必须事先设计好“协议”,例如在包头写明包长度,这样的话,在接收数据时精确地读取此长度的数据,就根本不会有所谓“粘包问题”。
    在这里插入图片描述

二、读数据时的 Short Read 问题

  • 从Socket输入流精确读取指定长度的数据时,使用InputStream.read(byte b[], int off, int len) 函数,其中第三个参数表示期望从输入流读取的字节长度,但实际成功读取的长度可能小于这个值,所以当读取数据不完整时,必须有==【补读】机制==。
    /**
     * 从输入流循环精确读取指定长度的数据
     * @param destLen - 目标长度
     * @return
     * @throws SocketException 
     */
    private static byte[] ExactRead(InputStream is,int destLen) throws SocketException {
    	ByteArrayOutputStream bout = new ByteArrayOutputStream();
    	try {
    		byte[] buffer = new byte[1024]; //大小为1024的缓冲区
    		//读取'size=商'的长度
    		for (int i = 0; i < (destLen / 1024); i++) {
    			//首次读取
    			int 首次尝试读取到的长度=is.read(buffer,0,1024);
    			// if return -1 , there is no more data because the end of the stream has been reached.
    			// 如果返回-1,则说明socket连接已断开
    			if(首次尝试读取到的长度==-1) {return null;} 
    			bout.write(buffer, 0, 首次尝试读取到的长度);
    			int  需补读的长度=1024-首次尝试读取到的长度;
    			//补读
    			if(需补读的长度 != 0) 
    			{
    				while(需补读的长度!=0) {
    					int 本次补读到的长度=is.read(buffer,0,需补读的长度);	
    					bout.write(buffer, 0, 本次补读到的长度);
    					需补读的长度 -= 本次补读到的长度; 
    				}
    			}
    		}
    		//读取'size=余数'的长度
    		if (destLen % 1024 != 0) {
    			int 首次尝试读取到的长度=is.read(buffer,0,destLen % 1024); 
    			// there is no more data because the end of the stream has been reached.
    			// 如果返回-1,则说明socket连接已断开
    			if(首次尝试读取到的长度==-1) {return null;}
    			bout.write(buffer, 0, 首次尝试读取到的长度);
    			int 需补读的长度= destLen % 1024 - 首次尝试读取到的长度;
    			if(需补读的长度!=0) 
    			{
    				while(需补读的长度!=0) {
    					int 本次补读到的长度=is.read(buffer,0,需补读的长度);
    					bout.write(buffer, 0, 本次补读到的长度);
    					需补读的长度-=本次补读到的长度; 
    				}
    			}
    		}
    	} 
    	catch (SocketException e) {
    		//如果连接断开,抛出异常
    		throw e;
    	}
    	catch (Exception e) {
    		e.printStackTrace();
    	}
    	
    	return bout.toByteArray();
    }
    

三、Socket读写的线程安全问题:应避免多线程对同一个Socket对象的读或写操作

  • 问题描述
    正常的发送数据流程:应用逻辑层先向Socket的输入流写入数据,然后socket将其输入流的数据发送出去(滑动窗口)。在这个过程中,Socket将输入流的数据发送的部分是线程安全的,不会出错。但从应用层向“Socket输入流”写入时并不是线程安全的!
    对于同一个socket对象,多线程同时向其“输入流”写入数据,那么数据可能是混杂的。例如,线程1写1000个’a’,线程2同时写1000个’b’,那么输入流中的数据可能是a、b交替混杂的。
  • 【解决方案一:加锁】
    对于需要多线程进行socket写操作的【Write的封装函数】加上同步锁。(但这又存在性能问题)
    场景:客户端只用一个socket连接服务器,但需要两个线程发送不同的数据,这时候就必须对客户端的"write封装函数"加同步锁了。
  • 【解决方案二:建立多个socket连接】(更好)
    客户端为每个线程都建立各自的socket连接,每个线程都收发各自的数据,这样就避免了多线程同时写一个socket引发的数据混杂问题和加锁引发的性能下降问题。
  • 【加锁方案】的客户端的write封装函数的代码
    	/**
    	 * write的封装函数:根据协议发送消息
    	 * @param os
    	 * @param msg
    	 * @throws Exception 
    	 */
    	private static Object lockObj=new Object(); //锁对象
    	public static void Send(OutputStream os,Message msg) throws Exception 
    	{
    		//只有客户端的write封装函数需加同步锁。因为客户端有多个线程同时对同一个socket对象写入数据。
    		synchronized (lockObj) {
    			//按照我的自定义协议发送数据
    			byte[] b_type=msg.getType().getBytes("UTF-8");
    			os.write(Convert.intToByteArray(msg.getContents().length+b_type.length+1),0,4);	//发送4字节的包头,表示接下来的正文长度
    			os.write(b_type,0,b_type.length);						//发正文:消息类型	
    			os.write('-');											//发正文:分隔符
    			os.write(msg.getContents(),0,msg.getContents().length);	//发正文:具体数据	
    		}			
    	}
    

参考资料

发布了56 篇原创文章 · 获赞 5 · 访问量 7446

猜你喜欢

转载自blog.csdn.net/forchoosen/article/details/103430067