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

风流意气都作罢 提交于 2020-05-04 12:03:59

之前写的实现简单网络通信的代码,有一些严重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  }

 

至此结束

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

部分界面截图




 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!