java简单的socket编程

前言,背景什么的不会写,要了解背景的,基础tcp/ip知识的百度吧,一大票那种文章,我这边就不写废话了,省得太长不看。

直接讲步骤和代码,有例子是最好理解的。

本篇分成2部分:一是client客户端、二是server服务端

先从服务端server讲起:

①创建ServerSocket对象

  serverSocket = new ServerSocket(port);

②监听接入,生成Socket对象

while (true) {
                        Socket socket = serverSocket.accept();
                        log.info("client连入成功:" + socket.getLocalPort());
                        new Thread(new ServerRunnable(socket)).start();
                    }

  

③启动对应的socket线程,供socket执行具体的业务逻辑

new Thread(new ServerRunnable(socket)).start();

内部类ServerRunnable:

private class ServerRunnable implements Runnable {
        
        private Socket       socket;
        
        private OutputStream writer;
        
        public ServerRunnable(Socket socket) throws IOException {
            
            super();
            this.socket = socket;
            this.writer = socket.getOutputStream();
        }
        
        @Override
        public void run() {
            
            // 服务端发送心跳包
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    
                    while (true) {
                        try {
                            Thread.sleep(1000 * 10 * 3);
                            if (!socket.isClosed()) {
                                int tag = CommonUtils.getNum();
                                queue.put(ServerSocketUtil.sendHeart(tag));
                            } else {
                                throw new Exception("socket关闭");
                            }
                        } catch (Exception e) {
                            log.error("client发送心跳包:"+e.getMessage(), e);
                            break;
                        }
                    }
                }
            }).start();
            
            new Thread(new ServerReadThread(socket, queue)).start();
            new Thread(new ServerWriteThread(socket, queue)).start();
        }
        
    }

  

以下是server类完整代码:

public class MyServerSocket {
    
    private Logger                                   log                  = Logger.getLogger(MyServerSocket.class);
    
    private int                                      port                 = 17921;
    
    private LinkedBlockingQueue<byte[]>              queue                = new LinkedBlockingQueue<byte[]>(10);
    
    private ServerSocket                             serverSocket         = null;
    
    public LinkedBlockingQueue<byte[]> getQueue() {
        
        return queue;
    }
    
    public void setQueue(LinkedBlockingQueue<byte[]> queue) {
        
        this.queue = queue;
    }
    
    public void start() {
        
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                
                try {
                    serverSocket = new ServerSocket(port);
                    log.info("服务端服务启动监听:" + port);
                    while (true) {
                        Socket socket = serverSocket.accept();
                        log.info("client连入成功:" + socket.getLocalPort());
                        new Thread(new ServerRunnable(socket)).start();
                    }
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        }).start();
    }
    
    private class ServerRunnable implements Runnable {
        
        private Socket       socket;
        
        private OutputStream writer;
        
        public ServerRunnable(Socket socket) throws IOException {
            
            super();
            this.socket = socket;
            this.writer = socket.getOutputStream();
        }
        
        @Override
        public void run() {
            
            // 服务端发送心跳包
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    
                    while (true) {
                        try {
                            Thread.sleep(1000 * 10 * 3);
                            if (!socket.isClosed()) {
                                int tag = CommonUtils.getNum();
                                queue.put(ServerSocketUtil.sendHeart(tag));
                            } else {
                                throw new Exception("socket关闭");
                            }
                        } catch (Exception e) {
                            log.error("client发送心跳包:"+e.getMessage(), e);
                            break;
                        }
                    }
                }
            }).start();
            
            new Thread(new ServerReadThread(socket, queue)).start();
            new Thread(new ServerWriteThread(socket, queue)).start();
        }
        
    }
}

  如果要用spring启动就需要建一个bean标签,将启动方法指定为start。

重点:接下来是读线程和写的线程,读写肯定要分成2个独立的线程,因为简单的socket是bio模式(阻塞io),read流是一个阻塞的流,会阻塞write操作,如果读写不分开,那个write就会被卡组,除非read有值读取,程序才能往下执行。

 

读线程:

public class ServerReadThread implements Runnable {
    
    private Logger                      log    = Logger.getLogger(ServerReadThread.class);
    
    private BufferedInputStream         reader = null;
    
    private LinkedBlockingQueue<byte[]> queue  = null;
    
    private Socket                      socket = null;
    
    /**
     * 服务端读取数据类 接收上报数据
     * 
     * @param socket
     * @param queue
     */
    public ServerReadThread(Socket socket, LinkedBlockingQueue<byte[]> queue) {
        
        super();
        this.queue = queue;
        this.socket = socket;
        try {
            this.reader = new BufferedInputStream(socket.getInputStream());
        } catch (IOException e) {
            log.error("server read thread constructor:" + e.getMessage(), e);
        }
    }
    
    /**
     * 并下发命令
     * 
     * @param lineString
     * @throws InterruptedException
     */
    private void excuteReceiveDatas(String lineString) throws InterruptedException {
        log.info("当期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"---"+lineString);
    }
    
    @Override
    public void run() {
        
        while (true) {
            try {
//按行读取需要将流变成BufferedReader使用readLine方法,这个很简单,就不多赘述了。 // 解析数据包(根据自己业务规定的协议,按字节读取,根据长度解析,避免粘包) byte[] head = new byte[] { 0, 0, 0, 0 }; reader.read(head, 0, 2); byte[] len = new byte[] { 0, 0, 0, 0 }; reader.read(len, 0, len.length); int length = Int2ByteUtil.byteArrayToInt(len); int surplusLen = length-2-4;//-head-length //剩余字节长度 byte[] surplusByte = new byte[surplusLen]; reader.read(surplusByte, 0, surplusLen); /** * tag 0-3 * cmd 4,5 * ut 6 * s1 7 * s2 8 * s3 9 */ byte[] tag = new byte[] { 0, 0, 0, 0 }; tag[0]=surplusByte[0]; tag[1]=surplusByte[1]; tag[2]=surplusByte[2]; tag[3]=surplusByte[3]; /** * 指令 服务端心跳:0x03 客户端心跳回复:0x02 客户端发送消息:0x06 */ byte[] cmd = new byte[] { 0, 0, 0, 0 }; cmd[0]=surplusByte[4]; cmd[1]=surplusByte[5]; int command = Int2ByteUtil.byteArrayToInt(cmd); log.info("command:"+command); byte[] ut = new byte[] { 0, 0, 0, 0 }; ut[0] = surplusByte[6]; // 预留字段 byte[] s1 = new byte[] { 0, 0, 0, 0 }; byte[] s2 = new byte[] { 0, 0, 0, 0 }; byte[] s3 = new byte[] { 0, 0, 0, 0 }; int datalen = length - 0x10; byte[] data = null; if (datalen > 0) { data = new byte[datalen]; reader.read(data, 0, data.length); } if (command == 0x05) { // 客户端发送消息包处理(处理上报数据) String lineString = new String(data, "utf-8"); excuteReceiveDatas(lineString); } else if (command == 0x01) { queue.put(ServerSocketUtil.sendHeartReply(CommonUtils.getNum())); } else if (command == 0x04) { // 客户端回应服务端心跳 log.info("客户端收到服务端发送的心跳包"); } } catch (Exception e) { log.error("server read thread:" + e.getMessage(), e); try { reader.close();reader = null; socket.close();socket = null; } catch (IOException e1) { log.error(e1.getMessage(), e1); } break; } } } }

  

writer:

public class ServerWriteThread implements Runnable {
    
    private Logger                      log    = Logger.getLogger(ServerWriteThread.class);
    
    private BufferedOutputStream        writer = null;
    
    private LinkedBlockingQueue<byte[]> queue  = null;
    
    private Socket                      socket = null;
    
    /**
     * 服务端发送数据类 下发命令
     * 
     * @param socket
     * @param queue
     */
    public ServerWriteThread(Socket socket, LinkedBlockingQueue<byte[]> queue) {
        
        super();
        this.queue = queue;
        this.socket = socket;
        try {
            this.writer = new BufferedOutputStream(socket.getOutputStream());
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }
    
    @Override
    public void run() {
        
        while (true) {
            byte[] element = null;
            try {
                // 防止下次写数据的时候write关闭,将queue元素take出来
                if (!socket.isClosed()) {
                    element = queue.take();
                    //log.info("即将发送数据element:"+element.hashCode());
                    writer.write(element);
                    writer.flush();
                }else {
                    throw new Exception("socket关闭");
                }
            } catch (Exception e) {
                //将queue.take对象放回去
                log.error("server write thread:"+e.getMessage(), e);
                try {
                    //log.info("回收数据element:"+element.hashCode());
                    queue.put(element);
                    writer.close();
                } catch (IOException | InterruptedException e1) {
                    writer=null;
                    log.error("关闭writer异常:"+e1.getMessage(), e1);
                }
                break;
            }
        }
    }
    
}

  上文中有用到LinkedBlockingQueue这是一个线程安全的阻塞队列,负责数据传输。因为while循环中如果要传数据普通的函数传参是行不通的,所以用到了这个。

 

接下来是client:

public class MyClientSocket {
	private Logger log = Logger.getLogger(MyClientSocket.class);

	private int port = 17921;
	private String host = "localhost";
	private LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<byte[]>(100);
	//private LinkedBlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<JSONObject>(100);
	//下发消息的线程池
	private ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(20);
	
	
	private Socket socket = null;
    private OutputStream writer;

	public LinkedBlockingQueue<byte[]> getQueue() {
		return queue;
	}

	public void setQueue(LinkedBlockingQueue<byte[]> queue) {
		this.queue = queue;
	}

	public void start() {
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				new Thread(new ClientSocket()).start();
			}
		}).start();
	}
	
	public void sendHeartbeat(int code) {
		new Thread(new Runnable() {
		    //初始化赋值心跳write hashcode
		    private int writeHashCode = code;
			@Override
			public void run() {
				while (true) {
					try {
						Thread.sleep(1000 * 10 * 2);
						//如果socket被关闭就不发送
						if(!socket.isClosed() && writeHashCode == writer.hashCode()) {
							queue.put(ClientSocketUtil.sendHeart(CommonUtils.getNum()));
						}else {
						    log.error("socket关闭或者输出流不是同一个对象,关闭当前心跳线程");
						    break;
						}
					} catch (Exception e) {
						log.error("client发送心跳包:"+e.getMessage(), e);
						break;
					}
				}
			}
		}).start();
	}
	
	private class ClientSocket implements Runnable{
		@Override
		public void run() {
			while (true) {
				try {
					if (socket == null || socket.isClosed()) {
						socket = new Socket(host, port);//连接socket
						log.info("已连接");
						writer=socket.getOutputStream();
						if(socket!=null) {
						    new Thread(new ClientReadThread(socket,queue,threadPoolExecutor)).start();
	                        new Thread(new ClientWriteThread(socket,writer,queue)).start();
	                        sendHeartbeat(writer.hashCode());
						}
					}
				} catch (Exception e) {
					if(e instanceof ConnectException) {
						continue;
					}else {
						log.error("client socket:"+e.getMessage(), e);
					}
				}
			}
		}
	}
}

  以上代码中有用到线程池,这个是因为我这边业务需要,因为要群发命令给硬件,不能因为某个命令卡住了导致后面的都阻塞,所以用了线程池。

还有是关于断线重连的逻辑,大家可以看下,特别是心跳线程的处理,用了hashCode判断socket输出流是不是同一个对象,因为前一个socket断掉了,这个心跳线程其实还能跑,所以我是这么做的,如果有更好的做法希望大家能教教我。

reader和writer线程跟server的逻辑几乎一模一样。不贴了,贴了可能会影响观看。

逻辑其实很简单。

如有不足请大家指出,本人不才,献丑了。

posted on 2018-08-12 21:17  康纳酱  阅读(171)  评论(0)    收藏  举报

导航