1. 程式人生 > >java於網路:P2P聊天系統

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上吧