Java Socket 全双工通讯

最开始接触TCP编程是想测试一下服务器的一些端口有没有开,阿里云的服务器,公司的服务器,我也不知道他开了那些端口,因而写个小程序测试一下,反正就是能连上就是开了,java

虽然晓得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 }

这是客户端的代码socket

 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 }

无聊的时候就本身和本身聊天吧ide

感受在这基础上能够搭个http服务器什么的了函数

 

而后就能够输入要返回的信息了,输入完断开客户端链接就行了,就是 2:exit,而后浏览器就能看到返回的信息了,不过貌似没有响应头,只有响应正文

/*

这里什么都没写

还有服务器或者客户端添加个执行远程命令什么的方法。。。。。。

别老想坏事,没开SSH的服务器远程执行个运维脚本什么的也不错啊,尤为是Win的服务器

其实我一直想弄个远程部署Tomcat项目的东西,最好是热部署,否则每次都要用FTP上传war

可是Windows服务器不会玩

*/

目前已知Bug:

当一方(不管是客户端仍是服务器)输入消息后但没有发出,但此时接受到另外一方发来的消息,显示会出现问题

由于输入的字符还在缓冲区中,因此会看到本身正在写的字符和发来的字符拼到了一行

左边是服务器,右边是客户端

客户端输入了 `测试一下` 但没有发出,服务器此时发送一条消息 `这里是服务器` 因而就发生了右图的状况

而后客户端发送消息,服务器收到 `测试一下`,发送前再输入字符不会影响到服务器接受到的消息,

例如在上述状况下,客户端收到服务器的消息后,在输入`我还没说完` 而后再发送,服务器会收到 `测试一下我还没说完`

也就是说只是客户端要发送的消息,显示上会与服务器发来的消息显示在一行,并且再输入字符会折行

若是谁知道怎么弄请告诉我一下

相关文章
相关标签/搜索