java socket实现服务端,客户端简单网络通信。Chat

时间:2022-09-07 20:24:39

之前写的实现简单网络通信的代码,有一些严重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连接。由于功能较简单,所以只用到了插入,查询语句。

客户端读入用户输入的登录,注册信息,发送至服务端,服务端在连接数据库进行查询/插入操作,将结果发送至客户端。

实例代码

 package logindata;

 import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList; public class LoginData implements Runnable{ static ArrayList<Socket> loginsocket = new ArrayList(); public LoginData() { } @Override
public void run() {
ServerSocket serverSocket=null;
try {
serverSocket = new ServerSocket(6567);
} catch (IOException e) {
e.printStackTrace();
}
while(true) {
Socket socket=null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
loginsocket.add(socket); Runnable runnable;
try {
runnable = new LoginDataIO(socket);
Thread thread = new Thread(runnable);
thread.start();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class LoginDataIO implements Runnable{ String b="false";
Socket socket;
DataInputStream inputStream;
DataOutputStream outputStream;
public LoginDataIO(Socket soc) throws IOException {
socket = soc;
inputStream = new DataInputStream(socket.getInputStream());
outputStream = new DataOutputStream(socket.getOutputStream());
} @Override
public void run() {
String readUTF = null;
String readUTF2 = null;
String readUTF3 = null;
try {
readUTF = inputStream.readUTF();
readUTF2 = inputStream.readUTF();
readUTF3 = inputStream.readUTF();
} catch (IOException e) {
e.printStackTrace();
} // System.out.println(readUTF+readUTF2+readUTF3); SqlServerCon serverCon = new SqlServerCon();
try {
//判断连接是登录还是注册,返回值不同。
if(readUTF3.equals("login")) {
b=serverCon.con(readUTF, readUTF2);
outputStream.writeUTF(b);
}else {
String re=serverCon.insert(readUTF, readUTF2);
outputStream.writeUTF(re);
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // System.out.println(b);
}
} class SqlServerCon { public SqlServerCon() {
// TODO Auto-generated constructor stub
} String name;
String password;
// boolean duge = false;
String duge = "false";
// String url = "jdbc:sqlserver://127.0.0.1:1433;"
// + "databaseName=TestData;user=sa;password=123456";
/**
* com.mysql.jdbc.Driver 更换为 com.mysql.cj.jdbc.Driver。
MySQL 8.0 以上版本不需要建立 SSL 连接的,需要显示关闭。
最后还需要设置 CST。
*/
//连接MySql数据库url格式
String url = "jdbc:mysql://127.0.0.1:3306/mytestdata?useSSL=false&serverTimezone=UTC";
public String con(String n,String p) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(url,"root","uu-7w3yfu?VX");
// System.out.println(connection); Statement statement = connection.createStatement();
// statement.executeUpdate("insert into Data values('china','123456')");
ResultSet executeQuery = statement.executeQuery("select * from persondata"); //登录昵称密码确认
while(executeQuery.next()) {
name=executeQuery.getString(1).trim();
password = executeQuery.getString(2).trim(); //"使用这个方法很重要" String trim() 返回值是此字符串的字符串,其中已删除所有前导和尾随空格。
// System.out.println(n.equals(name));
if(name.equals(n) && password.equals(p)) {
duge="true";
break;
}
}
statement.close();
connection.close();
// System.out.println(duge);
return duge;
} public String insert(String n,String p) throws SQLException, ClassNotFoundException {
boolean b = true;
String re = null;
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(url,"root","uu-7w3yfu?VX");
Statement statement = connection.createStatement(); ResultSet executeQuery = statement.executeQuery("select * from persondata");
while(executeQuery.next()) {
name=executeQuery.getString(1).trim();
// password = executeQuery.getString(2).trim();
if(name.equals(n)) {
b=false;
break;
}
} //返回登录信息
if(b && n.length()!=0 && p.length()!=0) {
String in = "insert into persondata "+"values("+"'"+n+"'"+","+"'"+p+"'"+")"; //这条插入语句写的很捞,但没想到更好的。
// System.out.println(in);
statement.executeUpdate(in);
statement.close();
connection.close();
re="注册成功,请返回登录";
return re;
}else if(n.length()==0 || p.length()==0 ) {
re="昵称或密码不能为空,请重新输入";
return re;
}else {
re="已存在该昵称用户,请重新输入或登录";
return re;
}
}
}

因为服务端需要放到服务器中,所以就删去了服务端的用户界面。

 import file.File;
import logindata.LoginData;
import server.Server; public class ServerStart_View { private static Server server = new Server();
private static File file = new File();
private static LoginData loginData = new LoginData();
public static void main(String [] args) {
ServerStart_View frame = new ServerStart_View();
server.get(frame);
Thread thread = new Thread(server);
thread.start(); Thread thread2 = new Thread(file);
thread2.start(); Thread thread3 = new Thread(loginData);
thread3.start();
}
public void setText(String AllName,String string) {
System.out.println(AllName+" : "+string);
}
}

客户端,登录界面与服务带进行socket连接,发送用户信息,并读取返回的信息。

主要代码:

 public class Login_View extends JFrame {

     public static String AllName=null;
static Login_View frame;
private JPanel contentPane;
private JTextField textField;
private JTextField textField_1;
JOptionPane optionPane = new JOptionPane();
private final Action action = new SwingAction();
private JButton btnNewButton_1;
private final Action action_1 = new SwingAction_1();
private JLabel lblNewLabel_2; /**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
frame = new Login_View();
frame.setVisible(true);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
} catch (Exception e) {
e.printStackTrace();
}
}
});
} ..................
..................
.................. private class SwingAction extends AbstractAction {
public SwingAction() {
putValue(NAME, "登录");
putValue(SHORT_DESCRIPTION, "点击登录");
}
public void actionPerformed(ActionEvent e) {
String text = textField.getText();
String text2 = textField_1.getText();
// System.out.println(text+text2);
// boolean boo=false;
String boo=null;
try {
boo = DataJudge.Judge(6567,text,text2,"login");
} catch (IOException e1) {
e1.printStackTrace();
}
if(boo.equals("true")) {
ClientStart_View.main1();
AllName = text; //保存用户名
frame.dispose(); //void dispose() 释放此this Window,其子组件和所有其拥有的子级使用的所有本机屏幕资源 。
}else {
optionPane.showConfirmDialog
(contentPane, "用户名或密码错误,请再次输入", "登录失败",JOptionPane.OK_CANCEL_OPTION);
}
}
} private class SwingAction_1 extends AbstractAction {
public SwingAction_1() {
putValue(NAME, "注册");
putValue(SHORT_DESCRIPTION, "点击进入注册页面");
}
public void actionPerformed(ActionEvent e) {
Registered_View registered = new Registered_View(Login_View.this);
registered.setLocationRelativeTo(rootPane);
registered.setVisible(true);
}
}
}

连接服务端:第一次写的时候连接方法是Boolean类型,但只适用于登录的信息判断,当注册时需要判断昵称是否重复,密码昵称是否为空等不同的返回信息,(服务端代码有相应的判断字符串返回,参上)于是该为将连接方法改为String类型。

 import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException; public class DataJudge { /*public static boolean Judge(int port,String name,String password,String judge) throws UnknownHostException, IOException { Socket socket = new Socket("127.0.0.1", port);
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); outputStream.writeUTF(name);
outputStream.writeUTF(password);
outputStream.writeUTF(judge); boolean readBoolean = inputStream.readBoolean(); outputStream.close();
inputStream.close();
socket.close();
return readBoolean;
}*/ public static String Judge(int port,String name,String password,String judge) throws UnknownHostException, IOException { //连接服务端数据库部分
Socket socket = new Socket("127.0.0.1", port);
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); outputStream.writeUTF(name);
outputStream.writeUTF(password);
outputStream.writeUTF(judge); String read = inputStream.readUTF(); //登录是一次性的,所以要及时关闭socket
outputStream.close();
inputStream.close();
socket.close();
return read;
}
}

用户注册界面,主要代码:

 public class Registered_View extends JDialog{
// DataJudge dataJudge = new DataJudge();
private JTextField textField_1;
private JTextField textField;
JLabel lblNewLabel_2;
private final Action action = new SwingAction(); public Registered_View(JFrame frame) {
super(frame, "", true); //使注册对话框显示在主面板之上。
.........
.........
.........
.........
} private class SwingAction extends AbstractAction {
public SwingAction() {
putValue(NAME, "注册");
putValue(SHORT_DESCRIPTION, "点击按钮进行注册");
}
public void actionPerformed(ActionEvent e) {
String b=null; //用于接收服务端返回的注册信息字符串
String name = textField.getText();
String password = textField_1.getText();
try {
b = DataJudge.Judge(6567, name, password, "registered");
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} lblNewLabel_2.setText(b);
}
}

用户登录,注册部分至此完毕。

实时显示人数,主要是向客户端返回存储socket对象的泛型数组大小。在当有新的客户端连接之后调用此方法,当有用户断开连接后调用此方法。

 public static void SendInfo(String rece, String AllName, String num) throws IOException {
DataOutputStream outputStream = null;
for (Socket Ssocket : Server.socketList) {
outputStream = new DataOutputStream(Ssocket.getOutputStream());
outputStream.writeUTF(num);
outputStream.writeUTF(AllName);
outputStream.writeUTF(rece);
outputStream.flush();
}
}

说说Bug

用户每次断开连接之前都没有先进行socket的关闭,服务端也没有移除相应的socket对象,这就导致当服务端再逐个发送至每个客户端,便找不到那个关闭的socket对象,会产生"write error" 。

所以便需要再客户端断开时移除相应的socket对象,查看java API文档,并没有找到在服务端可以判断客户端socket是否关闭的方方法。

java socket实现服务端,客户端简单网络通信。Chat

便想到了之前看的方法。(虽然感觉这样麻烦了一步,但没找到更好的办法)。于是在点击退出按钮,或关闭面板时向服务端发送一个"bye"字符,当服务端读取到此字符时便知道客户端要断开连接了,从而退出循环读取操作,移除对应的socket对象。

 面板关闭事件监听

 @Override
public void windowClosing(WindowEvent arg0) {
try {
chat_Client.send("bye");
File_O.file_O.readbye("bye");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
 退出按钮事件监听

 private class SwingAction extends AbstractAction {
public SwingAction() {
putValue(NAME, "退出");
putValue(SHORT_DESCRIPTION, "关闭程序");
}
public void actionPerformed(ActionEvent e) {
int result=optionPane.showConfirmDialog(contentPane, "是否关闭退出", "退出提醒", JOptionPane.YES_NO_OPTION);
if(result==JOptionPane.YES_OPTION) {
try {
chat_Client.send("bye");
File_O.file_O.readbye("bye");
System.exit(EXIT_ON_CLOSE); //static void exit​(int status) 终止当前正在运行的Java虚拟机。即终止当前程序,关闭窗口。
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
 客户端send方法,发送完bye字符后,关闭socket

 //send()方法,发送消息给服务器。 “发送”button 按钮点击事件,调用此方法
public void send(String send) throws IOException {
DataOutputStream stream = new DataOutputStream(socket.getOutputStream());
stream.writeUTF(Login_View.AllName);
stream.writeUTF(send); if(send.equals("bye")) {
stream.flush();
socket.close();
}
}
 服务端读取到bye字符时,移除相应socket对象,退出while循环

 if (rece.equals("bye")) {
judg = false;
Server.socketList.remove(socket);
Server_IO.SendInfo("", "", "" + Server.socketList.size());
/*
* for (Socket Ssocket:Server.socketList) { DataOutputStream outputStream = new
* DataOutputStream(socket.getOutputStream()); outputStream = new
* DataOutputStream(Ssocket.getOutputStream());
* outputStream.writeUTF(""+Server.socketList.size());
* outputStream.writeUTF(""); outputStream.writeUTF("");
* System.out.println("8888888888888888"); outputStream.flush(); }
*/
break;
}

文件的流的关闭,移除也是如此,不在赘述。

文件流还有一个问题,正常登录不能进行第二次文件传输。(第一次写的时候可能我只测试了一次,没有找到bug。哈哈哈哈)

解决这个问题耽搁了好久(太cai了,哈哈哈哈)

原来的代码,服务端读取并发送部分(也可参加看之前的随笔)

   while((len=input.read(read,0,read.length))>0) {
for(Socket soc:File.socketList_IO) {
if(soc != socket)
{
output = new DataOutputStream(soc.getOutputStream());
output.writeUTF(name);
output.write(read,0,len);
output.flush();
// System.out.println("开始向客户机转发");
}
}
// System.out.println("执行");
// System.out.println(len);
}

read()方法:API文档的介绍

java socket实现服务端,客户端简单网络通信。Chat

java socket实现服务端,客户端简单网络通信。Chat

当读取到文件末尾时会返回-1,可以看到while循环也是当len等于-1时结束循环,然而事与愿违。在debug时(忘记截图)发现,只要客户端的输出流不关闭,服务端当文件的读取完毕后会一直阻塞在

while((len=input.read(read,0,read.length))>0),无法退出,从而无法进行下一次读取转发。也无法使用len=-1进行中断break;
修改如下:
 int len=0;
while(true) {
len=0;
if(input.available()!=0)
len=input.read(read,0,read.length);
if(len==0) break;
for(Socket soc:File.socketlist_file) {
if(soc != socket)
{
output = new DataOutputStream(soc.getOutputStream());
output.writeUTF(name);
output.write(read,0,len);
// output.flush();
// System.out.println("开始向客户机转发");
}
// System.out.println("一次转发"+File.socketlist_file.size());
17 }
}

至此结束

感觉文件的传输读取仍然存在问题,下次继续完善。

部分界面截图

java socket实现服务端,客户端简单网络通信。Chat

java socket实现服务端,客户端简单网络通信。Chat