• java socket实现服务端,客户端简单网络通信。Chat


    之前写的实现简单网络通信的代码,有一些严重bug。后面详细写。

    根据上次的代码,主要增加了用户注册,登录页面,以及实现了实时显示当前在登录状态的人数。并解决一些上次未发现的bug。(主要功能代码参见之前随笔 https://www.cnblogs.com/yuqingsong-cheng/p/12740307.html)

    实现用户注册登录就需要用到数据库,因为我主要在学Sql Server。Sql Server也已支持Linux系统。便先在我的电脑Ubuntu系统下进行安装配置。

    链接:https://docs.microsoft.com/zh-cn/sql/linux/quickstart-install-connect-red-hat?view=sql-server-ver15     

    Sql Server官网有各个系统的安装指导文档,所以按照正常的安装步骤,一切正常安装。

    可放到服务器中却出现了问题。阿里云学生服务器是2G内存的(做活动外加学生证,真的很香。但内存有点小了)。sqlserer需要至少2G内存。所以只能放弃SqlServer,转向Mysql。

    同样根据MySql的官方指导文档进行安装。但进行远程连接却需要一些“乱七八糟”的配置,于是开始“面向百度连接”,推荐一个解决方案,https://blog.csdn.net/ethan__xu/article/details/89320614     适用于mysql8.0以上版本。

    数据库部分解决,开始写关于登录,注册类。登录注册部分新开了一个端口进行socket连接。由于功能较简单,所以只用到了插入,查询语句。

    客户端读入用户输入的登录,注册信息,发送至服务端,服务端在连接数据库进行查询/插入操作,将结果发送至客户端。

    实例代码

      1 package logindata;
      2 
      3 import java.io.DataInputStream;
      4 import java.io.DataOutputStream;
      5 import java.io.IOException;
      6 import java.net.ServerSocket;
      7 import java.net.Socket;
      8 import java.sql.Connection;
      9 import java.sql.DriverManager;
     10 import java.sql.ResultSet;
     11 import java.sql.SQLException;
     12 import java.sql.Statement;
     13 import java.util.ArrayList;
     14 
     15 public class LoginData implements Runnable{
     16 
     17     static ArrayList<Socket> loginsocket = new ArrayList();
     18     
     19     public LoginData() { }
     20 
     21     @Override
     22     public void run() {
     23         ServerSocket serverSocket=null;
     24         try {
     25             serverSocket = new ServerSocket(6567);
     26         } catch (IOException e) {
     27             e.printStackTrace();
     28         }
     29         while(true) {
     30             Socket socket=null;
     31             try {
     32                 socket = serverSocket.accept();
     33             } catch (IOException e) {
     34                 // TODO Auto-generated catch block
     35                 e.printStackTrace();
     36             }
     37             loginsocket.add(socket);
     38             
     39             Runnable runnable;
     40             try {
     41                 runnable = new LoginDataIO(socket);
     42                 Thread thread = new Thread(runnable);
     43                 thread.start();
     44             } catch (IOException e) {
     45                 // TODO Auto-generated catch block
     46                 e.printStackTrace();
     47             }
     48         }
     49     }
     50 }
     51 
     52 class LoginDataIO implements Runnable{
     53 
     54     String b="false";
     55     Socket socket;
     56     DataInputStream inputStream;
     57     DataOutputStream outputStream;
     58     public LoginDataIO(Socket soc) throws IOException {
     59         socket = soc;
     60         inputStream = new DataInputStream(socket.getInputStream());
     61         outputStream = new DataOutputStream(socket.getOutputStream());
     62     }
     63     
     64     @Override
     65     public void run() {
     66         String readUTF = null;
     67         String readUTF2 = null;
     68         String readUTF3 = null;
     69         try {
     70             readUTF = inputStream.readUTF();
     71             readUTF2 = inputStream.readUTF();
     72             readUTF3 = inputStream.readUTF();
     73         } catch (IOException e) {
     74             e.printStackTrace();
     75         }
     76         
     77 //        System.out.println(readUTF+readUTF2+readUTF3);
     78         
     79         SqlServerCon serverCon = new SqlServerCon();
     80         try {
     81             //判断连接是登录还是注册,返回值不同。
     82             if(readUTF3.equals("login")) {
     83                 b=serverCon.con(readUTF, readUTF2);
     84                 outputStream.writeUTF(b);
     85             }else {
     86                 String re=serverCon.insert(readUTF, readUTF2);    
     87                 outputStream.writeUTF(re);
     88             }
     89         } catch (SQLException e) {
     90             // TODO Auto-generated catch block
     91             e.printStackTrace();
     92         } catch (IOException e) {
     93             // TODO Auto-generated catch block
     94             e.printStackTrace();
     95         } catch (ClassNotFoundException e) {
     96             // TODO Auto-generated catch block
     97             e.printStackTrace();
     98         }  
     99         
    100 //        System.out.println(b);
    101     }
    102 }
    103 
    104 
    105 class SqlServerCon {
    106 
    107     public SqlServerCon() {
    108         // TODO Auto-generated constructor stub
    109     }
    110     
    111     String name;
    112     String password;
    113 //    boolean duge = false;
    114     String duge = "false";
    115 //    String url = "jdbc:sqlserver://127.0.0.1:1433;"
    116 //            + "databaseName=TestData;user=sa;password=123456";
    117     /**
    118      * com.mysql.jdbc.Driver 更换为 com.mysql.cj.jdbc.Driver。
    119         MySQL 8.0 以上版本不需要建立 SSL 连接的,需要显示关闭。
    120         最后还需要设置 CST。
    121      */
    122     //连接MySql数据库url格式
    123     String url = "jdbc:mysql://127.0.0.1:3306/mytestdata?useSSL=false&serverTimezone=UTC";
    124     public String con(String n,String p) throws SQLException, ClassNotFoundException {
    125         Class.forName("com.mysql.cj.jdbc.Driver");
    126         Connection connection = DriverManager.getConnection(url,"root","uu-7w3yfu?VX");
    127 //        System.out.println(connection);
    128         
    129         Statement statement = connection.createStatement();
    130 //        statement.executeUpdate("insert into Data values('china','123456')");
    131         ResultSet executeQuery = statement.executeQuery("select * from persondata");
    132         
    133         //登录昵称密码确认
    134         while(executeQuery.next()) {
    135             name=executeQuery.getString(1).trim();
    136             password = executeQuery.getString(2).trim();   //"使用这个方法很重要"  String     trim()      返回值是此字符串的字符串,其中已删除所有前导和尾随空格。
    137 //            System.out.println(n.equals(name));
    138             if(name.equals(n) && password.equals(p)) {
    139                 duge="true";
    140                 break;
    141             }
    142         }
    143         statement.close();
    144         connection.close();
    145 //        System.out.println(duge);
    146         return duge;
    147     }
    148     
    149     public String insert(String n,String p) throws SQLException, ClassNotFoundException {
    150         boolean b = true;
    151         String re = null;
    152         Class.forName("com.mysql.cj.jdbc.Driver");
    153         Connection connection = DriverManager.getConnection(url,"root","uu-7w3yfu?VX");
    154         Statement statement = connection.createStatement();
    155         
    156         ResultSet executeQuery = statement.executeQuery("select * from persondata");
    157         while(executeQuery.next()) {
    158             name=executeQuery.getString(1).trim();
    159 //            password = executeQuery.getString(2).trim();  
    160             if(name.equals(n)) {
    161                 b=false;
    162                 break;
    163             }
    164         }
    165         
    166         //返回登录信息
    167         if(b && n.length()!=0 && p.length()!=0) {
    168             String in = "insert into persondata "+"values("+"'"+n+"'"+","+"'"+p+"'"+")";  //这条插入语句写的很捞,但没想到更好的。
    169 //            System.out.println(in);
    170             statement.executeUpdate(in);
    171             statement.close();
    172             connection.close();
    173             re="注册成功,请返回登录";
    174             return re;
    175         }else if(n.length()==0 || p.length()==0 ) {
    176             re="昵称或密码不能为空,请重新输入";
    177             return re;
    178         }else {
    179             re="已存在该昵称用户,请重新输入或登录";
    180             return re;
    181         }
    182     }
    183 }

    因为服务端需要放到服务器中,所以就删去了服务端的用户界面。

     1 import file.File;
     2 import logindata.LoginData;
     3 import server.Server;
     4 
     5 public class ServerStart_View {
     6     
     7     private static Server server = new Server();
     8     private static File file = new File();
     9     private static LoginData loginData = new LoginData();
    10     public static void main(String [] args) {
    11         ServerStart_View frame = new ServerStart_View();
    12         server.get(frame);
    13         Thread thread = new Thread(server);
    14         thread.start();
    15         
    16         Thread thread2 = new Thread(file);
    17         thread2.start();
    18         
    19         Thread thread3 = new Thread(loginData);
    20         thread3.start();
    21     }
    22     public void setText(String AllName,String string) {
    23         System.out.println(AllName+" : "+string);
    24     }
    25 }

    客户端,登录界面与服务带进行socket连接,发送用户信息,并读取返回的信息。

    主要代码:

     1 public class Login_View extends JFrame {
     2 
     3     public static String AllName=null;
     4     static Login_View frame;
     5     private JPanel contentPane;
     6     private JTextField textField;
     7     private JTextField textField_1;
     8     JOptionPane optionPane = new JOptionPane();
     9     private final Action action = new SwingAction();
    10     private JButton btnNewButton_1;
    11     private final Action action_1 = new SwingAction_1();
    12     private JLabel lblNewLabel_2;
    13 
    14     /**
    15      * Launch the application.
    16      */
    17     public static void main(String[] args) {
    18         EventQueue.invokeLater(new Runnable() {
    19             public void run() {
    20                 try {
    21                     frame = new Login_View();
    22                     frame.setVisible(true);
    23                     frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
    24                 } catch (Exception e) {
    25                     e.printStackTrace();
    26                 }
    27             }
    28         });
    29     }
    30 
    31 ..................
    32 ..................
    33 ..................
    34 
    35 private class SwingAction extends AbstractAction {
    36         public SwingAction() {
    37             putValue(NAME, "登录");
    38             putValue(SHORT_DESCRIPTION, "点击登录");
    39         }
    40         public void actionPerformed(ActionEvent e) {
    41             String text = textField.getText();
    42             String text2 = textField_1.getText();
    43 //            System.out.println(text+text2);
    44 //            boolean boo=false;
    45             String boo=null;
    46             try {
    47                 boo = DataJudge.Judge(6567,text,text2,"login");
    48             } catch (IOException e1) {
    49                 e1.printStackTrace();
    50             }
    51             if(boo.equals("true")) {
    52                 ClientStart_View.main1();
    53                 AllName = text;    //保存用户名
    54                 frame.dispose();    //void    dispose()    释放此this Window,其子组件和所有其拥有的子级使用的所有本机屏幕资源 。
    55             }else {
    56                 optionPane.showConfirmDialog
    57                 (contentPane, "用户名或密码错误,请再次输入", "登录失败",JOptionPane.OK_CANCEL_OPTION);
    58             }
    59         }
    60     }
    61     
    62     private class SwingAction_1 extends AbstractAction {
    63         public SwingAction_1() {
    64             putValue(NAME, "注册");
    65             putValue(SHORT_DESCRIPTION, "点击进入注册页面");
    66         }
    67         public void actionPerformed(ActionEvent e) {
    68             Registered_View registered = new Registered_View(Login_View.this);
    69             registered.setLocationRelativeTo(rootPane);
    70             registered.setVisible(true);
    71         }
    72     }
    73 }

    连接服务端:第一次写的时候连接方法是Boolean类型,但只适用于登录的信息判断,当注册时需要判断昵称是否重复,密码昵称是否为空等不同的返回信息,(服务端代码有相应的判断字符串返回,参上)于是该为将连接方法改为String类型。

     1 import java.io.DataInputStream;
     2 import java.io.DataOutputStream;
     3 import java.io.IOException;
     4 import java.net.Socket;
     5 import java.net.UnknownHostException;
     6 
     7 public class DataJudge {
     8 
     9     /*public static boolean Judge(int port,String name,String password,String judge) throws UnknownHostException, IOException {
    10         
    11         Socket socket = new Socket("127.0.0.1", port);
    12         DataInputStream inputStream = new DataInputStream(socket.getInputStream());
    13         DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
    14         
    15         outputStream.writeUTF(name);
    16         outputStream.writeUTF(password);
    17         outputStream.writeUTF(judge);
    18         
    19         boolean readBoolean = inputStream.readBoolean();
    20         
    21         outputStream.close();
    22         inputStream.close();
    23         socket.close();
    24         return readBoolean;
    25     }*/
    26 
    27 public static String Judge(int port,String name,String password,String judge) throws UnknownHostException, IOException {
    28     
    29         //连接服务端数据库部分
    30         Socket socket = new Socket("127.0.0.1", port);
    31         DataInputStream inputStream = new DataInputStream(socket.getInputStream());
    32         DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
    33         
    34         outputStream.writeUTF(name);
    35         outputStream.writeUTF(password);
    36         outputStream.writeUTF(judge);
    37         
    38         String read = inputStream.readUTF();
    39         
    40         //登录是一次性的,所以要及时关闭socket
    41         outputStream.close();
    42         inputStream.close();
    43         socket.close();
    44         return read;
    45     }
    46 }

    用户注册界面,主要代码:

     1 public class Registered_View extends JDialog{
     2 //    DataJudge dataJudge = new DataJudge();
     3     private JTextField textField_1;
     4     private JTextField textField;
     5     JLabel lblNewLabel_2;
     6     private final Action action = new SwingAction();
     7     
     8     public Registered_View(JFrame frame) {
     9         super(frame, "", true);   //使注册对话框显示在主面板之上。
    10                 .........
    11                 .........
    12                 .........
    13                 .........
    14         }  
    15       
    16         private class SwingAction extends AbstractAction {
    17         public SwingAction() {
    18             putValue(NAME, "注册");
    19             putValue(SHORT_DESCRIPTION, "点击按钮进行注册");
    20         }
    21         public void actionPerformed(ActionEvent e) {
    22             String b=null;  //用于接收服务端返回的注册信息字符串
    23             String name = textField.getText();
    24             String password = textField_1.getText();
    25             try {
    26                 b = DataJudge.Judge(6567, name, password, "registered");
    27             } catch (IOException e1) {
    28                 // TODO Auto-generated catch block
    29                 e1.printStackTrace();
    30             }
    31             
    32             lblNewLabel_2.setText(b);
    33         }
    34     }

    用户登录,注册部分至此完毕。

    实时显示人数,主要是向客户端返回存储socket对象的泛型数组大小。在当有新的客户端连接之后调用此方法,当有用户断开连接后调用此方法。

     1 public static void SendInfo(String rece, String AllName, String num) throws IOException {
     2         DataOutputStream outputStream = null;
     3         for (Socket Ssocket : Server.socketList) {
     4             outputStream = new DataOutputStream(Ssocket.getOutputStream());
     5             outputStream.writeUTF(num);
     6             outputStream.writeUTF(AllName);
     7             outputStream.writeUTF(rece);
     8             outputStream.flush();
     9         }
    10     }

    说说Bug

    用户每次断开连接之前都没有先进行socket的关闭,服务端也没有移除相应的socket对象,这就导致当服务端再逐个发送至每个客户端,便找不到那个关闭的socket对象,会产生"write error" 。

    所以便需要再客户端断开时移除相应的socket对象,查看java API文档,并没有找到在服务端可以判断客户端socket是否关闭的方方法。

     便想到了之前看的方法。(虽然感觉这样麻烦了一步,但没找到更好的办法)。于是在点击退出按钮,或关闭面板时向服务端发送一个"bye"字符,当服务端读取到此字符时便知道客户端要断开连接了,从而退出循环读取操作,移除对应的socket对象。

     1 面板关闭事件监听
     2 
     3 @Override
     4     public void windowClosing(WindowEvent arg0) {
     5         try {
     6             chat_Client.send("bye");
     7             File_O.file_O.readbye("bye");
     8         } catch (IOException e) {
     9             // TODO Auto-generated catch block
    10             e.printStackTrace();
    11         }
    12     }
     1 退出按钮事件监听
     2 
     3 private class SwingAction extends AbstractAction {
     4         public SwingAction() {
     5             putValue(NAME, "退出");
     6             putValue(SHORT_DESCRIPTION, "关闭程序");
     7         }
     8         public void actionPerformed(ActionEvent e) {
     9             int result=optionPane.showConfirmDialog(contentPane, "是否关闭退出", "退出提醒", JOptionPane.YES_NO_OPTION);
    10             if(result==JOptionPane.YES_OPTION) {
    11                 try {
    12                     chat_Client.send("bye");
    13                     File_O.file_O.readbye("bye");
    14                     System.exit(EXIT_ON_CLOSE);  //static void    exit​(int status)    终止当前正在运行的Java虚拟机。即终止当前程序,关闭窗口。
    15                 } catch (IOException e1) {
    16                     e1.printStackTrace();
    17                 }
    18             }
    19         }
    20     }
     1 客户端send方法,发送完bye字符后,关闭socket
     2 
     3 //send()方法,发送消息给服务器。 “发送”button 按钮点击事件,调用此方法
     4     public void send(String send) throws IOException {
     5         DataOutputStream stream = new DataOutputStream(socket.getOutputStream());
     6         stream.writeUTF(Login_View.AllName);
     7         stream.writeUTF(send);
     8         
     9         if(send.equals("bye")) {
    10             stream.flush();
    11             socket.close();
    12         }
    13     }
     1 服务端读取到bye字符时,移除相应socket对象,退出while循环
     2 
     3 if (rece.equals("bye")) {
     4                             judg = false;
     5                             Server.socketList.remove(socket);
     6                             Server_IO.SendInfo("", "", "" + Server.socketList.size());
     7                             /*
     8                              * for (Socket Ssocket:Server.socketList) { DataOutputStream outputStream = new
     9                              * DataOutputStream(socket.getOutputStream()); outputStream = new
    10                              * DataOutputStream(Ssocket.getOutputStream());
    11                              * outputStream.writeUTF(""+Server.socketList.size());
    12                              * outputStream.writeUTF(""); outputStream.writeUTF("");
    13                              * System.out.println("8888888888888888"); outputStream.flush(); }
    14                              */
    15                             break;
    16                         }

    文件的流的关闭,移除也是如此,不在赘述。

    文件流还有一个问题,正常登录不能进行第二次文件传输。(第一次写的时候可能我只测试了一次,没有找到bug。哈哈哈哈)

    解决这个问题耽搁了好久(太cai了,哈哈哈哈)

    原来的代码,服务端读取并发送部分(也可参加看之前的随笔)

     1   while((len=input.read(read,0,read.length))>0) {
     2                for(Socket soc:File.socketList_IO) {
     3                       if(soc != socket)
     4                              { 
     5                                  output = new DataOutputStream(soc.getOutputStream());
     6                                  output.writeUTF(name);
     7                                  output.write(read,0,len);
     8                                  output.flush();
     9  //                                System.out.println("开始向客户机转发");
    10                              }
    11                          }
    12  //                        System.out.println("执行");
    13  //                        System.out.println(len);
    14                      }

    read()方法:API文档的介绍

     

    当读取到文件末尾时会返回-1,可以看到while循环也是当len等于-1时结束循环,然而事与愿违。在debug时(忘记截图)发现,只要客户端的输出流不关闭,服务端当文件的读取完毕后会一直阻塞在

    while((len=input.read(read,0,read.length))>0),无法退出,从而无法进行下一次读取转发。也无法使用len=-1进行中断break;
    修改如下:
     1 int len=0;
     2 while(true) {
     3     len=0;
     4     if(input.available()!=0)
     5        len=input.read(read,0,read.length);
     6     if(len==0) break;
     7     for(Socket soc:File.socketlist_file) {
     8        if(soc != socket)
     9        {
    10           output = new DataOutputStream(soc.getOutputStream());
    11           output.writeUTF(name);
    12           output.write(read,0,len);
    13 //        output.flush();
    14 //        System.out.println("开始向客户机转发");
    15        }
    16 //     System.out.println("一次转发"+File.socketlist_file.size());
    17     }
    18  }

    至此结束

    感觉文件的传输读取仍然存在问题,下次继续完善。

    部分界面截图



  • 相关阅读:
    django权限管理(Permission)
    记一次sentry部署过程
    Virtualbox+Vagrant环境准备
    jquery操作select(取值,设置选中)
    mysql 5.7主从安装和配置
    vue环境安装
    python 打印堆栈信息方法
    python3模块: os
    mysql查询语句(mysql学习笔记七)
    mysql存储引擎(mysql学习六)
  • 原文地址:https://www.cnblogs.com/yuqingsong-cheng/p/12822628.html
Copyright © 2020-2023  润新知