java于网络:P2P聊天系统
之前学习完了网络和java跟网络的相关知识,想试着写点东西,可又无从下手....于是就跟着书上完成了这个聊天系统
该系统能够提供聊天和多人聊天,只要输入注册名和IP地址注册和选择聊天对象即可
信息服务器
信息服务器需要不断的检测新的客户端发来的请求,并且为已经连接的客户端提供服务,所以需要不断的执行1.接收请求 2.解析请求 3.发送响应这三个操作,解析请求又会根据不同类型的请求发送不同的响应
收到的请求 | 服务器完成的操作 | 发出的响应 | |
1.客服端注册 | 1.从服务器保存的客户端端信息中查阅是否有此人 | 2.已存在 | 1."该名字已被注册" |
2.注册,并保存注册名,IP地址 | 1.注册成功 | ||
2.获取在线客户端 | 从服务器中生成在线客户端列表 | 2.返回列表 | |
3..获得聊天对象的IP地址 | 通过注册名从服务器查找对应的IP地址 | 3.返回聊天对象的IP地址 | |
4.客户端退出服务器 | 从服务器中删除该客户端的信息 | 4."已经退出" |
服务器不断检测新的客户端发送的请求,每当有新的客服端注册,服务器就会生成一个子线程,只为该客户端服务。
客户端与服务器通信时,传递注册名和地址要保证传送的准确性和可靠,故选择TCP连接客户端和服务器,使用Socket对象,客户端与客户端之间进行通信时,要求实时性,不需要无比的准确,故选择UDP连接客户端与客户端,使用DatagramSocket对象进行通信,DatagramPacket为数据包
UDP为无连接传输,在DatagramSocket传送DatagramPacket时,只需要知道IP地址和端口号即可进行通信
服务器实现如下
服务器有两个类,MessageServer类和MessageHandler类。MessageServer类实现主程序,MessageHandler类是子线程的线程体类,定义了子线程所需完成的各种方法
本例的ServerSocket构造函数,只定义了端口号,没有定义IP地址,IP地址采用默认的地址,即0.0.0.0,表示所有的IP地址,就表示ServerSocket监听在本机的所有IP地址上,通过任何一个IP地址都可以访问到.如果只想访问特定的IP地址,可以进行设置
MessageServer 类
public class MessageServer {
public static final int PORT=8000;//固定端口号
public static final int MAX_QUEUE_LENGTH=100;
public void start(){
try{
ServerSocket s=new ServerSocket(PORT, MAX_QUEUE_LENGTH);
System.out.println("****服务器已经启动...****");
while(true){
Socket socket=s.accept();//监听是否有客户端的连接,如果有,返回socket对象
System.out.println("已接收到客户来自: "+socket.getInetAddress());
MessageHandler handler=new MessageHandler(socket);//为每个新连接的客户端创建一个子线程
handler.start();//启动线程
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
MessageServer ms=new MessageServer();
ms.start();
}
}
MessageHandler 类
public class MessageHandler implements Runnable {
private Socket socket;
//与聊天端的通信时的输入,输出端
private ObjectInputStream datainput;
private ObjectOutputStream dataoutput;
private Thread listener;
private static Hashtable<String,InetSocketAddress>clientMessage= new Hashtable<>();//保存p2p注册名和地址
private Request request;//请求变量
private Response response;//响应变量
private boolean keepListening=true;
//创建客户端子线程的线程体
public MessageHandler(Socket socket){
this.socket=socket;
}
public synchronized void start(){
if(listener==null){
try{
//初始化
datainput=new ObjectInputStream(socket.getInputStream());
dataoutput=new ObjectOutputStream(socket.getOutputStream());
listener=new Thread(this);
listener.start();
}catch (IOException e){
e.printStackTrace();
}
}
}
public synchronized void stop(){
if(listener!=null){
try{
listener.interrupt();;
listener=null;
datainput.close();
dataoutput.close();
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
public void run() {
try {
while(keepListening){
receiveRequest();//接收请求
parseRequest();//解析请求
sendResponse();//发送响应
request=null;
}
stop();
}catch (ClassNotFoundException e){
e.printStackTrace();
}catch (IOException e){
stop();
System.err.println("与客户端通信出现错误...");
}
}
private void receiveRequest()throws IOException,ClassNotFoundException{
request=(Request)datainput.readObject();//从客户端接收请求
}
private void parseRequest(){
if(request==null)
return;
response=null;
int requestType=request.getRequestTyper();
String registerName=request.getRegisterName();
if(requestType!=1&&!registerNameHasBeenUsed(registerName)){
response=new Response(1,registerName+"你还未注册!" );
return;
}
switch (requestType){//测试请求类型
case 1:
if(registerNameHasBeenUsed(registerName)){
response=new Response(1,"|"+registerName+"|"+"已被其他人使用,请使用其他名字注册" );
break;
}
clientMessage.put(registerName, new InetSocketAddress(socket.getInetAddress(), request.getUDPPort()));
response=new Response(1,registerName+",你已经注册成功!" );
System.out.println("|"+registerName+"| 注册成功...");
break;
case 2:
Vector<String> allNameOfRegister= new Vector<>();
for(Enumeration<String>e=clientMessage.keys();e.hasMoreElements(); ){
//生成已注册的P2P端注册名列表
allNameOfRegister.addElement(e.nextElement());
}
response=new Response(2,allNameOfRegister );
break;
case 3:
String chatRegisterName=request.getChatRegisterName();
InetSocketAddress chatP2PEndAddress=clientMessage.get(chatRegisterName);
response=new Response(3, chatP2PEndAddress);
break;
case 4:
clientMessage.remove(registerName);
response=new Response(1,registerName+",你已经从服务器退出!" );
keepListening=false;
System.out.println("|"+registerName+"| 从服务器退出...");
}
}
private boolean registerNameHasBeenUsed(String registerName){
if(registerName!=null&&clientMessage.get(registerName)!=null)
return true;
return false;
}
private void sendResponse()throws IOException{
if(response!=null){
dataoutput.writeObject(response);//将响应写回聊天端
}
}
}
请求类和响应类
创建Request类和Respone类来封装请求信息和响应信息,Request类和Respone类的对象需要在网络中传输,需要进行序列化,实现Serializable接口
Request类
public class Request implements Serializable {
private int requestTyper;//请求类型
private String registerName;//注册名
private int UDPPort;//端口号
private String chatRegisterName;//聊天对象的注册名
public Request(int requestTyper,String registerName){
this.requestTyper=requestTyper;
this.registerName=registerName;
}
public Request(int requestTyper,String registerName, int UDPPort){
this(requestTyper,registerName);
this.UDPPort=UDPPort;
}
public Request(int requestTyper,String registerName, String chatRegisterName){
this(requestTyper,registerName);
this.chatRegisterName=chatRegisterName;
}
public int getRequestTyper() {
return requestTyper;
}
public String getRegisterName() {
return registerName;
}
public int getUDPPort() {
return UDPPort;
}
public String getChatRegisterName() {
return chatRegisterName;
}
}
Response 类
public class Response implements Serializable {
private int responseType;
private String message;//响应信息
private Vector<String> allNameOfRegister;//存放所有客户端注册名的集合
private InetSocketAddress chatP2PEndAddress;//聊天对象的地址
public Response(int responseType){
this.responseType=responseType;
}
public Response(int responseType, String message) {
this.responseType = responseType;
this.message = message;
}
public Response(int responseType, Vector<String> allNameOfRegister) {
this.responseType = responseType;
this.allNameOfRegister = allNameOfRegister;
}
public Response(int responseType, InetSocketAddress chatP2PEndAddress) {
this.responseType = responseType;
this.chatP2PEndAddress = chatP2PEndAddress;
}
public int getResponseType() {
return responseType;
}
public String getMessage() {
return message;
}
public Vector<String> getAllNameOfRegister() {
return allNameOfRegister;
}
public InetSocketAddress getChatP2PEndAddress() {
return chatP2PEndAddress;
}
}
聊天端的实现
1.P2PChatEnd类实现了主界面
public class P2PChatEnd extends JFrame {
private Register register;
private GetOnlineP2PEnds getOnlineP2PEnds;
private Chat chat;
private JLabel label;
private JTabbedPane tabbedPane;
private Exit exit;
private CommWithServer commWithServer;//与服务器通信的线程
public P2PChatEnd(){
setTitle("P2P聊天端");
label=new JLabel();
label.setText("P2P聊天端");
label.setForeground(Color.blue);
label.setFont(new Font("隶书", Font.BOLD, 22));
label.setHorizontalTextPosition(SwingConstants.RIGHT);
label.setBackground(Color.green);
commWithServer=new CommWithServer();
register=new Register(commWithServer);
getOnlineP2PEnds=new GetOnlineP2PEnds(commWithServer);
chat=new Chat(this);
register.setChat(chat);
exit=new Exit(commWithServer,this);
tabbedPane=new JTabbedPane(JTabbedPane.LEFT);
tabbedPane.add("系统封面",label);
tabbedPane.add("注册信息服务器",register);
tabbedPane.add("选择聊天对象",getOnlineP2PEnds);
tabbedPane.add("聊天",chat);
tabbedPane.add("退出信息服务器",exit);
add(tabbedPane,BorderLayout.CENTER);
setBounds(120,60,400,147 );
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
new P2PChatEnd();
}
}
2.Register类实现了客户端的注册
public class Register extends JPanel implements ActionListener {
private JLabel hintLabel;
private JTextField registerNameField,serverIPField;//注册名和IP地址文本框
private JButton submint;//提交按钮
private CommWithServer commWithServer;
private Chat chat;
private Request request;
private Response response;
//使用对象流接收和发送响应和请求
private ObjectOutputStream pipedOut;
private ObjectInputStream pipedIn;
private int clickNum=0;
private boolean isRegister=false;//判读是否注册
public Register(CommWithServer commWithServer){
this.commWithServer=commWithServer;
setLayout(new BorderLayout());
hintLabel=new JLabel("注册",JLabel.CENTER);
hintLabel.setFont(new Font("隶书", Font.BOLD, 18));
registerNameField=new JTextField(10);
serverIPField=new JTextField(10);
submint=new JButton("提交");
submint.addActionListener(this);
Box box1=Box.createHorizontalBox();
box1.add(new JLabel("注 册 名: ",JLabel.CENTER));
box1.add(registerNameField);
Box box2= Box.createHorizontalBox();
box2.add(new JLabel("服 务 器IP: ",JLabel.CENTER));
box2.add(serverIPField);
Box boxH=Box.createVerticalBox();
boxH.add(box1);
boxH.add(box2);
boxH.add(submint);
JPanel panelC=new JPanel();
panelC.setBackground(new Color(210,210,110 ));
panelC.add(boxH);
add(panelC,BorderLayout.CENTER);
JPanel panelN=new JPanel();
panelN.setBackground(Color.green);
panelN.add(hintLabel);
add(panelN,BorderLayout.NORTH);
}
public void setChat(Chat chat){
this.chat=chat;
}
public void actionPerformed(ActionEvent e) {
if(isRegister){
String hint="不能重复注册";
JOptionPane.showMessageDialog(this, hint,"警告",JOptionPane.WARNING_MESSAGE);
clear();
return;
}
clickNum++;
String registerName=registerNameField.getText().trim();
String serverIP=serverIPField.getText().trim();
if (registerName.length()==0||serverIP.length()==0){
String hint="必须输入注册名和服务器IP";
JOptionPane.showMessageDialog(this, hint,"警告",JOptionPane.WARNING_MESSAGE);
clear();
return;
}
try {
if(clickNum==1){
//使用管道通信,让线程commWithServer可以和该类进行通信
PipedInputStream pipedI=new PipedInputStream();
PipedOutputStream pipedO=new PipedOutputStream(pipedI);
//序列化和反序列化
pipedOut=new ObjectOutputStream(pipedO);
pipedIn=new ObjectInputStream(pipedI);
}
DatagramSocket socket=new DatagramSocket();
Chat.setSocket(socket);
int UDPPort=socket.getLocalPort();//获得一个UDP端口号
request=new Request(1, registerName,UDPPort);//封装请求
if(commWithServer!=null){
if(commWithServer.isAlive()){//线程已经启动,已与信息服务器连接
commWithServer.close();//断开与信息服务器的连接
//连接信息服务器,pipedOut传递给commWithServer,commWithServer再将响应写到缓冲器
commWithServer.connect(serverIP,request,pipedOut);
commWithServer.notifyCommWithServer();//将线程唤醒
}else{
commWithServer.connect(serverIP,request,pipedOut);//连接信息服务器
commWithServer.start();//启动线程,与信息服务器通信
}
}
//pipedIn读取缓存区的响应
response=(Response)pipedIn.readObject();
}catch (Exception ex){
JOptionPane.showMessageDialog(this, "无法连接或与服务器通信出错","警告",JOptionPane.WARNING_MESSAGE);
clear();
return;
}
String message=response.getMessage();
boolean flag=true;
if(message!=null&&message.equals(request.getRegisterName()+",你已经注册成功!")){
message+="请单击左侧的\"获取在线P2P端\"";
flag=false;
}
JOptionPane.showMessageDialog(null, message,"信息提示",JOptionPane.PLAIN_MESSAGE);
if(flag){//注册没有成功,清除单行文本域,返回重新注册
clear();
return;
}
/*注册成功,将注册名传递给GetOnlineP2PEnds类对象,Chat类对象和Exit对象*/
GetOnlineP2PEnds.setRegisterName(registerName);
Chat.setRegisterName(registerName);
Exit.setRegisterName(registerName);
isRegister=true;//设置注册成功标志,控制不能重复注册
//建立并启动"从其他P2P端接收信息"的子线程,等待接收信息
new Thread(chat).start();
clear();
}
private void clear(){
registerNameField.setText(" ");
serverIPField.setText(" ");
}
}
3.GetOnlineP2PEnds类
public class GetOnlineP2PEnds extends JPanel implements ActionListener {
private JButton getOnlineP2PEnds,submit;
private JList list;
private CommWithServer commWithServer;
private Request request;
private Response response;
private ObjectOutputStream pipedOut;
private ObjectInputStream pipedIn;
private static String registerName;
private int clickNum=0;
public GetOnlineP2PEnds(CommWithServer commWithServer){
this.commWithServer=commWithServer;
setLayout(new BorderLayout());
getOnlineP2PEnds=new JButton("获取在线P2P端");
getOnlineP2PEnds.setBackground(Color.green);
submit=new JButton("提 交");
submit.setBackground(Color.green);
getOnlineP2PEnds.addActionListener(this);
submit.addActionListener(this);
list=new JList();
list.setFont(new Font("楷体", Font.BOLD, 15));
JScrollPane scroll=new JScrollPane();
scroll.getViewport().setView(list);
Box box=Box.createHorizontalBox();
box.add(new JLabel("单击 '获取' :",JLabel.CENTER));
box.add(getOnlineP2PEnds);
JPanel panelR=new JPanel(new BorderLayout());
panelR.setBackground(new Color(201,210,110 ));
panelR.add(submit,BorderLayout.SOUTH);
JPanel panel=new JPanel(new BorderLayout());
panel.setBackground(new Color(210,210,110 ));
panel.add(box,BorderLayout.NORTH);
panel.add(new JLabel("选择聊天P2P端:"),BorderLayout.WEST);
panel.add(scroll,BorderLayout.CENTER);
panel.add(panelR,BorderLayout.EAST);
add(panel,BorderLayout.CENTER);
submit.setEnabled(false);
validate();
}
public static void setRegisterName(String name){
registerName=name;
}
public void actionPerformed(ActionEvent e) {
if(registerName==null||commWithServer==null||!commWithServer.isAlive()){
JOptionPane.showMessageDialog(null, "你还没有注册!","信息提示",JOptionPane.PLAIN_MESSAGE);
return;
}
try{
if(e.getSource()==getOnlineP2PEnds){
clickNum++;
if(clickNum==1){
PipedInputStream pipedI=new PipedInputStream();
PipedOutputStream pipedO=new PipedOutputStream(pipedI);
pipedOut=new ObjectOutputStream(pipedO);
pipedIn=new ObjectInputStream(pipedI);
}
request=new Request(2, registerName);
commWithServer.setRequest(request);
commWithServer.setPipedOut(pipedOut);
commWithServer.notifyCommWithServer();;
response=(Response)pipedIn.readObject();
//从响应中得到在线的P2P端注册名列表
Vector<String> onLineP2PEnds=response.getAllNameOfRegister();
//尝试将null值传递给此方法会导致未定义的行为,并且最有可能发生异常。 创建的模型直接引用给定的
// Vector 。调用此方法后尝试修改Vector会导致未定义的行为。
list.setListData(onLineP2PEnds);
submit.setEnabled(true);
}
if(e.getSource()==submit){
List<Object> list2=list.getSelectedValuesList();
int len=list2.size();
if(len==0){
JOptionPane.showMessageDialog(this, "你还未选择聊天P2P端!","信息提示",JOptionPane.PLAIN_MESSAGE);
return;
}
String register[]=new String[list2.size()];
for(int i=0;i<list2.size();i++)
register[i]=(String)list2.get(i);
Vector<InetSocketAddress> P2PEndAddress=new Vector<>();
int chatP2PEnds=0;
for(int i=0;i<len;i++){
if(register[i].equals(registerName))//如果聊天对象名与当前相同,则跳过
continue;
request=new Request(3, registerName, register[i]);
commWithServer.setRequest(request);
commWithServer.setPipedOut(pipedOut);
commWithServer.notifyCommWithServer();
response=(Response)pipedIn.readObject();
//以下代码将从响应中得到的聊天对象地址加入到列表中
P2PEndAddress.add(response.getChatP2PEndAddress());
chatP2PEnds++;
}
String message=null;
if(chatP2PEnds==0){
message="你只选择了与自己聊天,请重新选择聊天端!";
}else{
Chat.setChatP2PEndAddress(P2PEndAddress);
message="已获取到你选择P2P端的地址,请单击左侧的|聊天|按钮";
}
JOptionPane.showMessageDialog(this, message,"信息提示",JOptionPane.PLAIN_MESSAGE);
P2PEndAddress.clear();//清空地址列表
list.setListData(P2PEndAddress);
}
}catch (Exception e1){
JOptionPane.showMessageDialog(this, "与服务器通信出错","警告",JOptionPane.WARNING_MESSAGE);
}
}
}
写到一半发现把代码都放在博客上不现实,太长了,还是放在GitHub上吧