Java Socket 全双工通信
最开始接触TCP编程是想测试一下服务器的一些端口有没有开,阿里云的服务器,公司的服务器,我也不知道他开了那些端口,于是写个小程序测试一下,反正就是能连上就是开了,
虽然晓得nmap这些东西,但服务器不监听开放的端口,他也检测不到开没开
后来前几天写了个程序,接受TCP请求并解析字节流写入数据库,这其实不难,整个程序就是个半双工模式,就是设备给我发一条消息,我给他回一条
然后就像写个类似QQ这类聊天软件的东西玩玩,百度了半天没找到全双工的例子,那就自己写吧,两天写完了,好开心,有新玩具可以玩了
不解释,直接放代码,感觉注释写的很清楚了
这是服务器的代码
补充一点,可能忘写了,服务器可以主动断开与客户端的连接,例如连接的id是1号,那么输入1:exit,就会断开与id为1的连接
1 import java.io.*; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 import java.util.Set; 5 import java.util.Map; 6 import java.util.HashMap; 7 import java.util.LinkedList; 8 9 /** 10 * 服务器,全双工,支持单播和广播 11 * 12 * 注意是全双工,全双工,全双工 13 * 14 * 就是像QQ一样 15 */ 16 public class Server{ 17 // 分配给socket连接的id,用于区分不同的socket连接 18 private static int id = 0; 19 // 存储socket连接,发送消息的时候从这里取出对应的socket连接 20 private HashMap<Integer,ServerThread> socketList = new HashMap<>(); 21 // 服务器对象,用于监听TCP端口 22 private ServerSocket server; 23 24 /** 25 * 构造函数,必须输入端口号 26 */ 27 public Server(int port) { 28 try { 29 this.server = new ServerSocket(port); 30 System.out.println("服务器启动完成 使用端口: "+port); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 } 35 36 /** 37 * 启动服务器,先让Writer对象启动等待键盘输入,然后不断等待客户端接入 38 * 如果有客户端接入就开一个服务线程,并把这个线程放到Map中管理 39 */ 40 public void start() { 41 new Writer().start(); 42 try { 43 while (true) { 44 Socket socket = server.accept(); 45 System.out.println(++id + ":客户端接入:"+socket.getInetAddress() + ":" + socket.getPort()); 46 ServerThread thread = new ServerThread(id,socket); 47 socketList.put(id,thread); 48 thread.run(); 49 } 50 } catch (IOException e) { 51 e.printStackTrace(); 52 } 53 } 54 55 /** 56 * 回收资源啦,虽然不广播关闭也没问题,但总觉得通知一下客户端比较好 57 */ 58 public void close(){ 59 sendAll("exit"); 60 try{ 61 if(server!=null){ 62 server.close(); 63 } 64 }catch(IOException e){ 65 e.printStackTrace(); 66 } 67 System.exit(0); 68 } 69 70 /** 71 * 遍历存放连接的Map,把他们的id全部取出来,注意这里不能直接遍历Map,不然可能报错 72 * 报错的情况是,当试图发送 `*:exit` 时,这段代码会遍历Map中所有的连接对象,关闭并从Map中移除 73 * java的集合类在遍历的过程中进行修改会抛出异常 74 */ 75 public void sendAll(String data){ 76 LinkedList<Integer> list = new LinkedList<>(); 77 Set<Map.Entry<Integer,ServerThread>> set = socketList.entrySet(); 78 for(Map.Entry<Integer,ServerThread> entry : set){ 79 list.add(entry.getKey()); 80 } 81 for(Integer id : list){ 82 send(id,data); 83 } 84 } 85 86 /** 87 * 单播 88 */ 89 public void send(int id,String data){ 90 ServerThread thread = socketList.get(id); 91 thread.send(data); 92 if("exit".equals(data)){ 93 thread.close(); 94 } 95 } 96 97 // 服务线程,当收到一个TCP连接请求时新建一个服务线程 98 private class ServerThread implements Runnable { 99 private int id; 100 private Socket socket; 101 private InputStream in; 102 private OutputStream out; 103 private PrintWriter writer; 104 105 /** 106 * 构造函数 107 * @param id 分配给该连接对象的id 108 * @param socket 将socket连接交给该服务线程 109 */ 110 ServerThread(int id,Socket socket) { 111 try{ 112 this.id = id; 113 this.socket = socket; 114 this.in = socket.getInputStream(); 115 this.out = socket.getOutputStream(); 116 this.writer = new PrintWriter(out); 117 }catch(IOException e){ 118 e.printStackTrace(); 119 } 120 } 121 122 /** 123 * 因为设计为全双工模式,所以读写不能阻塞,新开线程进行读操作 124 */ 125 @Override 126 public void run() { 127 new Reader().start(); 128 } 129 130 /** 131 * 因为同时只能有一个键盘输入,所以输入交给服务器管理而不是服务线程 132 * 服务器负责选择socket连接和发送的消息内容,然后调用服务线程的write方法发送数据 133 */ 134 public void send(String data){ 135 if(!socket.isClosed() && data!=null && !"exit".equals(data)){ 136 writer.println(data); 137 writer.flush(); 138 } 139 } 140 141 /** 142 * 关闭所有资源 143 */ 144 public void close(){ 145 try{ 146 if(writer!=null){ 147 writer.close(); 148 } 149 if(in!=null){ 150 in.close(); 151 } 152 if(out!=null){ 153 out.close(); 154 } 155 if(socket!=null){ 156 socket.close(); 157 } 158 socketList.remove(id); 159 }catch(IOException e){ 160 e.printStackTrace(); 161 } 162 } 163 164 /** 165 * 因为全双工模式所以将读操作单独设计为一个类,然后开个线程执行 166 */ 167 private class Reader extends Thread{ 168 private InputStreamReader streamReader = new InputStreamReader(in); 169 private BufferedReader reader = new BufferedReader(streamReader); 170 171 @Override 172 public void run(){ 173 try{ 174 String line = ""; 175 // 只要连接没有关闭,而且读到的行不为空,为空说明连接异常断开,而且客户端发送的不是exit,那么就一直从连接中读 176 while(!socket.isClosed() && line!=null && !"exit".equals(line)){ 177 line=reader.readLine(); 178 if(line!=null){ 179 System.out.println(id+":client: "+line); 180 } 181 } 182 // 如果循环中断说明连接已断开 183 System.out.println(id+":客户端主动断开连接"); 184 close(); 185 }catch(IOException e) { 186 System.out.println(id+":连接已断开"); 187 }finally{ 188 try{ 189 if(streamReader!=null){ 190 streamReader.close(); 191 } 192 if(reader!=null){ 193 reader.close(); 194 } 195 close(); 196 }catch(IOException e){ 197 e.printStackTrace(); 198 } 199 } 200 } 201 } 202 } 203 204 /** 205 * 因为发送的时候必须指明发送目的地,所以不能交给服务线程管理写操作,不然就无法选择向哪个连接发送消息 206 * 如果交给服务线程管理的话,Writer对象的会争夺键盘这一资源,谁抢到是谁的,就无法控制消息的发送对象了 207 */ 208 private class Writer extends Thread{ 209 // 我们要从键盘获取发送的消息 210 private BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 211 212 @Override 213 public void run(){ 214 String line = ""; 215 // 先来个死循环,除非主动输入exit关闭服务器,否则一直等待键盘写入 216 while(true){ 217 try{ 218 line = reader.readLine(); 219 if("exit".equals(line)){ 220 break; 221 } 222 }catch(IOException e){ 223 e.printStackTrace(); 224 } 225 // 输入是有规则的 [连接id]:[要发送的内容] 226 // 连接id可以为*,代表所有的连接对象,也就是广播 227 // 要发送的内容不能为空,发空内容没意义,而且浪费流量 228 // 连接id和要发送的消息之间用分号分割,注意是半角的分号 229 // 例如: 1:你好 ==>客户端看到的是 server:你好 230 // *:吃饭了 ==>所有客户端都能看到 server:吃饭了 231 if(line!=null){ 232 try{ 233 String[] data = line.split(":"); 234 if("*".equals(data[0])){ 235 // 这里是广播 236 sendAll(data[1]); 237 }else{ 238 // 这里是单播 239 send(Integer.parseInt(data[0]),data[1]); 240 } 241 // 有可能发生的异常 242 }catch(NumberFormatException e){ 243 System.out.print("必须输入连接id号"); 244 }catch(ArrayIndexOutOfBoundsException e){ 245 System.out.print("发送的消息不能为空"); 246 }catch(NullPointerException e){ 247 System.out.print("连接不存在或已经断开"); 248 } 249 } 250 } 251 // 循环中断说明服务器退出运行 252 System.out.println("服务器退出"); 253 close(); 254 } 255 } 256 257 public static void main(String[] args) { 258 int port = Integer.parseInt(args[0]); 259 new Server(port).start(); 260 } 261 }
这是客户端的代码
1 import java.io.*; 2 import java.net.Socket; 3 import java.net.UnknownHostException; 4 5 /** 6 * 客户端 全双工 但同时只能连接一台服务器 7 */ 8 public class Client { 9 private Socket socket; 10 private InputStream in; 11 private OutputStream out; 12 13 /** 14 * 启动客户端需要指定地址和端口号 15 */ 16 private Client(String address, int port) { 17 try { 18 socket = new Socket(address, port); 19 this.in = socket.getInputStream(); 20 this.out = socket.getOutputStream(); 21 } catch (UnknownHostException e) { 22 e.printStackTrace(); 23 } catch (IOException e) { 24 e.printStackTrace(); 25 } 26 System.out.println("客户端启动成功"); 27 } 28 29 public void start(){ 30 // 和服务器不一样,客户端只有一条连接,能省很多事 31 Reader reader = new Reader(); 32 Writer writer = new Writer(); 33 reader.start(); 34 writer.start(); 35 } 36 37 public void close(){ 38 try{ 39 if(in!=null){ 40 in.close(); 41 } 42 if(out!=null){ 43 out.close(); 44 } 45 if(socket!=null){ 46 socket.close(); 47 } 48 System.exit(0); 49 }catch(IOException e){ 50 e.printStackTrace(); 51 } 52 } 53 54 private class Reader extends Thread{ 55 private InputStreamReader streamReader = new InputStreamReader(in); 56 private BufferedReader reader = new BufferedReader(streamReader); 57 58 @Override 59 public void run(){ 60 try{ 61 String line=""; 62 while(!socket.isClosed() && line!=null && !"exit".equals(line)){ 63 line=reader.readLine(); 64 if(line!=null){ 65 System.out.println("Server: "+line); 66 } 67 } 68 System.out.println("服务器主动断开连接"); 69 close(); 70 }catch(IOException e){ 71 System.out.println("连接已断开"); 72 }finally{ 73 try{ 74 if(streamReader!=null){ 75 streamReader.close(); 76 } 77 if(reader!=null){ 78 reader.close(); 79 } 80 close(); 81 }catch(IOException e){ 82 e.printStackTrace(); 83 } 84 } 85 } 86 } 87 88 private class Writer extends Thread{ 89 private PrintWriter writer = new PrintWriter(out); 90 private BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 91 92 @Override 93 public void run(){ 94 try{ 95 String line = ""; 96 while(!socket.isClosed() && line!=null && !"exit".equals(line)){ 97 line = reader.readLine(); 98 if("".equals(line)){ 99 System.out.print("发送的消息不能为空"); 100 }else{ 101 writer.println(line); 102 writer.flush(); 103 } 104 } 105 System.out.println("客户端退出"); 106 close(); 107 }catch(IOException e){ 108 System.out.println("error:连接已关闭"); 109 }finally{ 110 try{ 111 if(writer!=null){ 112 writer.close(); 113 } 114 if(reader!=null){ 115 reader.close(); 116 } 117 close(); 118 }catch(IOException e){ 119 e.printStackTrace(); 120 } 121 } 122 } 123 } 124 125 public static void main(String[] args) { 126 String address = args[0]; 127 int port = Integer.parseInt(args[1]); 128 new Client(address, port).start(); 129 } 130 }
无聊的时候就自己和自己聊天吧
感觉在这基础上可以搭个http服务器什么的了
然后就可以输入要返回的信息了,输入完断开客户端连接就好了,就是 2:exit,然后浏览器就能看到返回的信息了,不过貌似没有响应头,只有响应正文
/*
这里什么都没写
还有服务器或者客户端添加个执行远程命令什么的方法。。。。。。
别老想坏事,没开SSH的服务器远程执行个运维脚本什么的也不错啊,尤其是Win的服务器
其实我一直想弄个远程部署Tomcat项目的东西,最好是热部署,不然每次都要用FTP上传war
但是Windows服务器不会玩
*/
目前已知Bug:
当一方(不论是客户端还是服务器)输入消息后但没有发出,但此时接受到另一方发来的消息,显示会出现问题
因为输入的字符还在缓冲区中,所以会看到自己正在写的字符和发来的字符拼到了一行
左边是服务器,右边是客户端
客户端输入了 `测试一下` 但没有发出,服务器此时发送一条消息 `这里是服务器` 于是就发生了右图的情况
然后客户端发送消息,服务器收到 `测试一下`,发送前再输入字符不会影响到服务器接受到的消息,
例如在上述情况下,客户端收到服务器的消息后,在输入`我还没说完` 然后再发送,服务器会收到 `测试一下我还没说完`
也就是说只是客户端要发送的消息,显示上会与服务器发来的消息显示在一行,而且再输入字符会折行
如果谁知道怎么弄请告诉我一下