1.实验要求.
- 用Java图形用户界面编写聊天室服务器端和客户端, 支持多个客户端连接到一个服务器。每个客户端能够输入账号。
- 可以实现群聊(聊天记录显示在所有客户端界面)。
- 完成好友列表在各个客户端上显示。
- 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息。
- 服务器能够群发系统消息,能够强行让某些用户下线。
- 客户端的上线下线要求能够在其他客户端上面实时刷新。
2.实验平台及语言.
实验选择语言为Java,开发平台为Eclipse IDE for Enterprise Java Developers.
Version: 2019-06 (4.12.0)
3.项目完整代码.
JavaSocket编程开发聊天室ThreeStrikes.
4.设计思路.
- 整个项目采用Client-Server设计模式,粗略地可以分为两个部分,即客户端Client与服务器端Server。客户端与服务器端进行交互的方式是:客户端封装自己需要进行的操作,例如发送文本消息、发送窗口抖动、登录、退出等,成为一类抽象的请求-plea,每一种不同的plea实例化之后通过不同的标识属性值来进行区分,而后将请求送达服务器端。服务器端采用多线程策略,为每一个到达的客户端请求建立一个线程,专门为该客户端服务,接收到客户端传来的plea后,服务器端内部进行一系列的处理(判断该请求是否合法、是否正确,比如你不能向自己发送消息、你不能用错误的账号和密码进行登录),之后服务器端封装自己的处理结果,例如允许登录、允许客户端发送消息等,成为一类抽象的回答-reply,每一种不同的reply实例化之后也通过不同的标识属性值来进行区分,而后将回答返还给客户端。
- 在具体的实现过程中,我们不可能将所有的功能都实现在同一个类中。比方说,按照我们上面的分析,我们只设计4个类:Client、Server、Plea、Reply就从逻辑上完成了整个项目。但这显然是不合理的,每一个类中会有太多的功能,例如Client类中,既要进行登录、注册、聊天界面的设计,又要完成发送消息、接收消息等业务代码的实现,这样的代码即使不出差错地写出来了,也是不忍回首的。我的想法是,从界面设计入手。用户最先进入的界面是登录界面,而后产生分支——或是选择注册,或是选择直接进入聊天室,在界面设计的过程中,不断进行功能之间的解耦合(例如登录界面中就不会考虑到发送消息的功能),不断细化整个客户端的分类,同时也不断实现每一个界面上需要实现的功能。
- 下面我们从客户端Client部分入手,讲述整个项目的实现及其细节。
5.Client客户端.
ClientLauncher.
ClientLauncher是用户看到的客户端的入口,由于服务器端Server是需要先于客户端Client启动的,所以我们讲述时,会假定已经有一个"Server"已经在等待着为我们服务了。整个ClientLauncher的代码并不是很长,我们在下面直接给出其main函数:
public static void main(String[] args) {//Connect with a Server.Make_Connection();//Client appearance.JFrame.setDefaultLookAndFeelDecorated(true);JDialog.setDefaultLookAndFeelDecorated(true);try{String LookAndFeel = UIManager.getCrossPlatformLookAndFeelClassName();UIManager.setLookAndFeel(LookAndFeel);}catch(Exception e){e.printStackTrace();}new ClientLogin();}
可以看出ClientLauncher中实际上只做了两件事:1、进行客户端与服务端连接的初始化建立;2、给出整个UI的风格确定(即代码中的getCrossPlatformLookAndFeelClassName()
)。
在Make_Connection()
中,我们所做的事情就是从预先配置好的属性文件中,按名读取"IP"与"Port"用于建立从客户端到服务器端的Socket,并且通过该Socket我们能够对连接的输入流和输出流进行操作。关于属性文件的操作,可以参看《Eclipse项目中创建.properties属性文件及其中属性》。
注意到ClientLauncher的最后,我们通过new创建了一个ClientLogin对象,顾名思义,这就是用户登录的界面。
ClientLogin.
ClientLauncher以调出ClientLogin的界面结束,我们下面直接给出ClientLogin的构造器代码:
public ClientLogin()
{Initialize();setVisible(true);
}
由于需要GUI,所以我们让ClientLogin继承了JFrame类,这也是需要setVisible(true)让整个GUI显示出来的原因。Initialize()方法中进行的是整个登陆界面的控件摆放,包括输入账号、密码的输入框、登录按钮、注册按钮以及一个出于美观考虑的Logo,除此之外,Initialize()方法中还完成了各种控件和事件监听器的绑定。最后的GUI如下图所示:
界面设计的细节描述如下:
- 设置界面上的字体,示例代码以JLabel为例,实际上很多控件都可以通过setFonts()方法来进行字体更改。
Font myFont = new Font("Arial Rounded MT Bold",Font.BOLD,18);
JLabel ID_Label=new JLabel("Identity:");
ID_Label.setFont(myFont);
至于自己的电脑上已经安装了哪些字体,以Win10为例,可以在C:\Windows\Fonts文件夹中查看。
- 设置边框,例如上图中包含Login Info的边框。
JPanel MainLoginPanel=new JPanel();
Border border=BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
MainLoginPanel.setBorder(BorderFactory.createTitledBorder(border,"Login Info.",TitledBorder.CENTER,TitledBorder.TOP));
代码段中createEtchedBorder()的意思是创建蚀刻风格的边框,而下面依据中的createTitleBorder则是在已经创建好的蚀刻边框上加入文字信息,我们对比其中的参数以及上图中的最终UI效果,不难看出其中参数的作用。更多关于边框的归纳知识,可以参看这篇文章。
- 设置背景色,无法通过直接调用setBackground(Color)来实现,虽然调用这一方法确实设置了背景色,但你却无法看到它,因为呈现出来的是contentPane的背景色。问题的关键在于,JComponent控件中的Opaque属性默认是false的,Opaque意为不透明,该属性为false也就意味着这一控件透明,所以最终的GUI会透过该控件而显示出底层的颜色。
MainLoginPanel.setOpaque(true);
MainLoginPanel.setBackground(new Color(179, 220, 241));
关于Color,我们可以使用Java预定义的值,例如Color.BLUE、Color.BLACK,也可以向上述代码中,通过RGB值来创建一个Color实例。至于如何查看某处颜色的RGB值,可以参看图片任意点的RGB值。
- JButton的事件监听器绑定,这是GUI程序中很常见的一种操作。
RegisterButton.addActionListener(new ActionListener()
{@Overridepublic void actionPerformed(ActionEvent e) {// TODO 自动生成的方法存根new ClientRegister();}
});
通过addActionListener()方法,我们可以将一个"动作"绑定到一个按钮上,当这个按钮被点击时,就执行绑定到上面的动作。其中关于匿名内部类的语法,就不再赘述。
- 验证账号的正确性。上面给出的GUI界面中,有【Sign Up】和【Sign in】两个JButton,它们分别对应于进入注册界面注册一个新账号和使用已有的账号登录进入聊天室。我们先叙述ClientRegister类中的内容,而后再讲述,如何验证账号的正确性。
ClientRegister.
在ClientLogin的GUI上点击【Sign Up】会进入ClientRegister的界面,客户端注册类的构造器和客户端登录类完全一样,区别就在于它们各自的Initialize()方法中所进行的界面设计以及绑定到控件的动作。ClientRegister中涉及昵称、密码、再次确认密码、性别、头像这些输入型控件以及完成注册、重填信息以及取消注册这些辅助功能按钮的设置,最后的GUI界面如下所示:
界面设计上的细节描述如下:
- 空布局null,这种布局策略相较于我们常见的边界布局BorderLayout、流式布局FlowLayout来说,程序员需要做的事情变多了,但好处是控件的摆放更为灵活。对于一个控件,我们可以通过方法
setBounds(int x,int y,int width,int height)
来精确控制它在最终GUI中的位置。需要注意的是,setBounds()
方法中的参数不是相较于整个屏幕而言,而是相较于采用null布局的这个界面而言。
getContentPane().setLayout(null);
JLabel nickname = new JLabel("Nickname:");
nickname.setBounds(24,36,65,17);
getContentPane().add(nickname);
- 单选按钮JRadioButton,单选按钮并没有很复杂的用法,我们上述的GUI中用它来给用户提供性别选择。
Male = new JRadioButton("Male",true);
Female = new JRadioButton("Female");
Male.setBounds(248,31,60,25);
Female.setBounds(310, 31, 75, 25);
getContentPane().add(Male);
getContentPane().add(Female);
其中Male按钮的构造器中的第二个参数,是设置该按钮是否默认选中,并且该参数默认为false。
- 下拉框JComboBox,下拉框是一种很便捷的展示形式,我们这里用它来进行头像的展示,头像图片的.png文件都是存放在项目下的文件夹中的。下拉框的具体效果如下图所示:
Profile = new JComboBox();
Profile.setBounds(278,70,65,45);
Profile.setMaximumRowCount(3);
for(int i=0;i<11;++i)
{Profile.addItem(new ImageIcon("D:\\NewDesktop\\images\\" + i + ".png"));
}
Profile.setSelectedIndex(0);
上述代码段有关GUI中下拉框的部分,.png文件的位置是可以自由设置的。这里的两个方法setMaximunRowCount(int x)是指下列的视图中最多显示几个完整的item,我们从上面的下拉框效果图中也可以看出这一点,而setSelectedIndex(int x)则是设置默认选中的item的序列号(从0开始).
- 记录完成注册流程的用户,当用户完成了信息的正确输入之后点击了OK按钮,我们就需要将这一用户的信息保存到本地的.db文件(是服务器端Server完成这一工作的,客户端不应该拥有修改记录所有用户信息的.db文件,.db文件同样是在前面提到的属性文件中完成配置的),用于登录时的账号验证。显然,记录下完成注册的用户信息并不是客户端自己能够完成的事情,所以这里需要和服务器端Server进行交互。首先客户端向Server发送标记为【注册】的请求以及表示该用户的【抽象用户数据】,而后等待Server的回复。这里我们先不展开叙述Server所作的工作,理想地认为Server能够给出正确的回复(例如对于已有用户的识别)。客户端得到Server的回复之后,它检查这个回复,如果Server回复中标记为了【成功】,客户端的注册界面就给出注册成功的信息,否则引导用户进行重填或者退出注册。当一个用户进行登录时,客户端也会向Server发送一个标记为【登录】的请求以及表示这一用户的【抽象用户数据】,理想的Server就对这一请求进行处理,判断是否密码正确、是否存在这一用户以及该用户是否已经登录,从而给出回复信息,客户端再根据回复中的标记,给出错误信息或者成功进入聊天界面。
private void RealRegis(ADT_of_User newUser) throws IOException,ClassNotFoundException
{Plea plea = new Plea();plea.setAction("Register");plea.setData("User", newUser);Reply reply = ClientToServer.sendMessage(plea);ReplyPhase phase = reply.getPhase();switch(phase){case SUCCESS:ADT_of_User tempUser = (ADT_of_User)reply.getData("User");JOptionPane.showMessageDialog(ClientRegister.this, "Wow!You have got yourself's account : "+tempUser.getID(),"Success Regiter.",JOptionPane.INFORMATION_MESSAGE);this.setVisible(false);break;default:JOptionPane.showMessageDialog(ClientRegister.this, "What a pity!Something wrong has happened.","ERROR",JOptionPane.ERROR_MESSAGE);}
}
ClientChat简述.
在ClientLogin的GUI上点击【Sign in】会进入ClientChat的界面,也就是聊天室的主要界面,大同小异的构造器就不再赘述。ClientChat的最终GUI如下所示,我们给出了两个客户端的界面:
ClientChat的界面相较于前面的登录界面、注册界面都复杂了很多,主要分为五个板块:左上角的群聊消息区、左下角的编辑消息区、右上角的在线用户列表、右下角的【自己】信息区以及一些给出辅助功能的区域(例如有着发送和退出的按钮区、私聊和常见聊天室功能的按钮区以及系统状态的提示区)。并且在上面展示的GUI中,已经进行了消息发送的演示,两张图对比起来看,可以很清楚看出发送消息的时间、逻辑关系。
在JavaSocket编程开发聊天室Ⅱ中会详细叙述ClientChat的内容.