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