引言
本学期学习了面向对象的程序设计语言——Java。Java语言是一门非常高级的编程语言,其面向对象的性质相比C++更加的突出。Java语言的功能十分的强大,有着十分丰富的内置API可供调用,使编程者使用起来非常的方便,这也是Java程序员是目前最多的程序员的原因之一。Java语言的用途十分的广泛,可以用于图形用户界面编程、Web编程以及网络编程等等。但由于课时的限制,本学期只学了Java的基础语法和GUI编程,其他方面并涉及。
但作为一名大学生,尤其是将来想从事软件编程的工作,一味的满足于课堂的学习是远远不够的。因此,我自己学习了一下基于Java的网络编程,此次项目开发我便用Java的网络编程相关知识进行学习和开发。这样一来,以便检测和巩固自己对于该方面的知识,一遍后面更好的学习,作为一名软件学习者,该过程是必不可少的。
通过自己不断地学习与尝试,最终开发出了一套较为完整地聊天系统。
设计依据及框图
设计平台
jdk1.8.0_131.
IntelliJ IDEA 2018.3.
设计思想
Java最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket。网络上具有唯一标识的IP地址和端口组合在一起才能构成唯一能识别的标识符套接字。通信的两端都要有Socket,网络通信的实质就是Socket间的通信(数据在两个Socket间通过IO传输)。
在通信过程中需要用到两种通信协议,即UDP协议和TCP协议,UDP协议(用户数据报协议)是无连接的、不可靠的、无序的、速度快,TCP协议是面向连接的、可靠的、有序的、以字节流的方式发送数据,通过三次握手方式建立连接,形成传输数据的通道,在连接中进行大量数据的传输,效率会稍低。考虑到可靠性,此次开发采用的是TCP协议。
网络编程部分的难点是聊天系统涉及到多客户端之间通过服务器的通信,不仅服务器要使用多线程,而且每个服务线程都要求能够调取其它服务线程中的socket以便向其它客户端传递消息,这就需要专门的数据容器来储存所有服务线程。网络编程部分的另一个难点是消息的结构设计。由于一个socket只有一对输入输出流,来自客户端和服务器的各种不同类型消息都要通过这对流来传递给对方,所以服务器和客户端都要能根据消息的类型采取不同的动作。
办法解决思路如下:
-
服务器使用多线程工作,每个客户端都享有一个服务线程。
-
在服务端声明一个map容器,将用户名和对应的socket建立映射关系并储存在静态容器中。
-
客户端和服务器之间每次通信都是传递一个字符串,这个字符串的开头带上功能信息,比如群聊、私聊、更新列表、下线退出等,通过正则表达式对字符串进行解析。
-
在服务器线程类中用while循环监听客户端的消息,客户端中也用while循环监听服务端的消息。
系统整体结构框图
服务器就是一个中转站,每创建一个客户端服务器就开辟一个线程为该客户端服务,保证每个客户端都享有一个服务器的线程,客户端只需要与服务端进行通信,从而实现了客户端之间的间接通信。
系统结构图
各模块功能实现
服务端模块
1.创建 ServerSocket对象,绑定监听端口。
2.通过accept()方法监听客户端请求。
3.连接建立后,通过输入流读取客户端发送的请求信息。
4.将用户名和对应的socket存入静态容器。
5.更新用户列表。
6.开辟新的线程为客户端服务。
5.关闭相关资源。
代码:
package Server;
import ChatGUI.ServerGUI;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Vector;
/**
* Created with IntelliJ IDEA.
* User: xsx
* Date: 2020/5/4
* Description:服务端
*/
public class Server {
//声明一个Map集合来存储并将用户名和Socket映射起来
static Map<String,Socket> map = new HashMap<String,Socket>();
//声明一个容器用于存储用户名以展示在线人员列表
static Vector<String>list=new Vector<>();
//建立一个图形窗体
static ServerGUI serverGUI=new ServerGUI(map);
public static void ServerStart(){
try {
//声明一个ServerSocket对象,指定端口为8888
ServerSocket serversocket = new ServerSocket(8888);
for(int i=0;i<50;i++){
//限制50个线程
//设置监听
Socket soc=serversocket.accept();
//拿到客户端输入流,读取用户注册名
Scanner scanner = new Scanner(soc.getInputStream());
String name;
if(scanner.hasNext()) {
name = scanner.nextLine();
map.put(name,soc);//将用户添加到储存集合
list.add(name);//将用户名添加到图形列表
serverGUI.setList(list);//更新列表
serverGUI.setCount(" 当前在线人数:"+map.size());
serverGUI.setText("用户:"+name+"已上线");
uptatefrind();//更新好友列表
//开辟一个新线程
Thread exserver = new Thread(new ExcuteServer(soc, map,list));
exserver.start();
}
}
serversocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void uptate(String str,Vector<String> list){
//有客户退出,更新数据
serverGUI.setText("用户名为:"+str +"的用户下线了!!!");
list.removeElement(str);
serverGUI.setList(list);//更新列表
serverGUI.setCount(" 当前在线人数:"+map.size());
}
public static void uptatefrind(){
//更新好友列表
for(Map.Entry<String, Socket> stringSocketEntry:map.entrySet()) {
//遍历Map集合,给所有客户端发消息
try {
Socket socket = stringSocketEntry.getValue();
String username=stringSocketEntry.getKey();
PrintStream printStream = new PrintStream(socket.getOutputStream(), true);
printStream.println("clear:");//清空原有的好友列表
printStream.println("list:群聊");
for(Map.Entry<String, Socket> i:map.entrySet()) {
String name=i.getKey();
if(!name.equals(username))//不给自己发
printStream.println("list:"+name);
}
printStream.println("finish:");//好友列表发送完毕
} catch (IOException x) {
x.printStackTrace();
}
}
}
public static void main(String[] args){
ServerStart();
}
}
服务器线程类模块
3.2.1 监听客户端消息
用while循环持续监听客户端的消息。
代码:
public void run()
{
try {
//拿到客户端输入流,读取用户信息
Scanner scanner = new Scanner(client.getInputStream());
String string;
while(true){
if(scanner.hasNext()) {
string = scanner.nextLine();
//创建正则表达式
Pattern pattern = Pattern.compile("\r\n|\r|\n");
Matcher matcher = pattern.matcher(string);
string = matcher.replaceAll("");
//群聊
if(string.startsWith("群聊:")) {
String message = string.split("\\:")[1];
groupchat(message);
}
//用户退出
else if(string.startsWith("Q")) {
//先根据Socket知道用户名
String useName = getUsername(client);