java socket實現服務端,客戶端簡單網絡通訊。Chat

以前寫的實現簡單網絡通訊的代碼,有一些嚴重bug。後面詳細寫。html

根據上次的代碼,主要增長了用戶註冊,登陸頁面,以及實現了實時顯示當前在登陸狀態的人數。並解決一些上次未發現的bug。(主要功能代碼參見以前隨筆 http://www.javashuo.com/article/p-xekanfjt-nq.html)java

 

實現用戶註冊登陸就須要用到數據庫,由於我主要在學Sql Server。Sql Server也已支持Linux系統。便先在個人電腦Ubuntu系統下進行安裝配置。mysql

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

Sql Server官網有各個系統的安裝指導文檔,因此按照正常的安裝步驟,一切正常安裝。sql

可放到服務器中卻出現了問題。阿里雲學生服務器是2G內存的(作活動外加學生證,真的很香。但內存有點小了)。sqlserer須要至少2G內存。因此只能放棄SqlServer,轉向Mysql。數據庫

一樣根據MySql的官方指導文檔進行安裝。但進行遠程鏈接卻須要一些「亂七八糟」的配置,因而開始「面向百度鏈接」,推薦一個解決方案,http://www.javashuo.com/article/p-edgqcjlk-bc.html     適用於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  }

 

至此結束

感受文件的傳輸讀取仍然存在問題,下次繼續完善。

部分界面截圖

相關文章
相關標籤/搜索