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:

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

因为输入的字符还在缓冲区中,所以会看到自己正在写的字符和发来的字符拼到了一行

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

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

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

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

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

如果谁知道怎么弄请告诉我一下

来自1942年冬季攻势中的中央集团军的037号17吨救援拖车
原文地址:https://www.cnblogs.com/panther1942/p/8873766.html