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