• 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吨救援拖车
  • 相关阅读:
    Nginx配置文件详解
    JVM调优—性能分析神器-JProfiler详解
    Navicat Premium 15破解
    Nginx配置反向代理,负载均衡,动静分离,高可用
    Nginx安装和常用命令
    Spring中ApplicationContextAware的作用
    java中发起http和https请求
    MySql高可用架构
    matlab画3维meshgrid/plot3/mesh/surf的用法
    如何规范地编写一个MATLAB函数文件
  • 原文地址:https://www.cnblogs.com/panther1942/p/8873766.html
Copyright © 2020-2023  润新知