以前寫的實現簡單網絡通訊的代碼,有一些嚴重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 }
至此結束
感受文件的傳輸讀取仍然存在問題,下次繼續完善。
部分界面截圖