1. 程式人生 > >java socket實現服務端,客戶端簡單網路通訊。Chat

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  }

 

至此結束

感覺檔案的傳輸讀取仍然存在問題,下次繼續完善。

部分介面截圖



&n