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 }
至此结束
感觉文件的传输读取仍然存在问题,下次继续完善。
部分界面截图