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的逻辑几乎一模一样。不贴了,贴了可能会影响观看。
逻辑其实很简单。
如有不足请大家指出,本人不才,献丑了。