之前写的实现简单网络通信的代码,有一些严重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 }
至此结束
感觉文件的传输读取仍然存在问题,下次继续完善。
部分界面截图
来源:oschina
链接:https://my.oschina.net/u/4363067/blog/4264084