JAVA中使用Socket实现自定义协议、无服务器即时通讯(类似飞秋)

来源:互联网 发布:如何评价诗词 知乎 编辑:程序博客网 时间:2024/05/20 13:18

首先来说明一下怎么实现:

大家都知道,Socket是点对点通讯,必需得有一个服务器,一个客户端。比如有A和B两位大神,当A大神向B大神发送消息时,A大神即为客户端,B大神即为服务器。反过来也一样。这就意味着,A大神即充当着客户端的角色,也充当着服务器的角色,只有这样他才能又能收又能发!而因为每个人都是服务器,也是客户端,所以没有必要使用一个专门的服务器处理消息!于是,无服务器的设想就诞生了!

那么,怎么样才能知道A大神向B大神发的是消息还是文件或者其它的什么东西(如视频请求)呢?嗯,想了想,既然HTTP有协议,FTP和蓝牙WIFI都有其协议,我们为什么不自定义一种协议来区别多种请求呢?这个设想肯定是可以的!

如何实现自定义协议?众所周知,HTTP协议是有报头和主体的区别的,受到这个启发,我们也可以定义一个协议,该协议包含报头和主体两部分。即报头用来指明发送的请求类型,而主体才是消息内容!

那么,如何区分报头和主体?我的想法很简单,因为Socket是通过网络流传送消息,那么,我们可以规定,流传递的消息中,前1024个字节为报头,以后的字节为消息主体!OK,万事具备,只欠编码了!

写界面的Swing代码太多,也没什么技术含量,在此不就贴贴出来了,只贴出主体部分代码。

1、消息发送线程,由于使用自已定义的协议,规定前1024个字节为报头,所以在发送时分两段发送:

/** * <pre> * Title: GuQiuSendThread.java * Project: GuQiu * Type:leoly.threads.GuQiuSendThread * Author:255507 * Create: 2011-9-7 下午03:49:38 * Copyright: Copyright (c) 2011 * Company: * <pre> */package leoly.threads;import java.io.File;import java.io.FileInputStream;import java.io.OutputStream;import java.net.InetAddress;import java.net.InetSocketAddress;import java.net.Socket;import java.text.MessageFormat;import javax.swing.JLabel;import javax.swing.JTextArea;import leoly.utils.GuQiuConstants;import leoly.utils.GuQiuUtils;/** * <pre> * </pre> * @author 255507 * @version 1.0, 2011-9-7 */public class GuQiuSendThread extends Thread{private JTextArea chatPanel;private JLabel fileLabel;private int msgType = GuQiuConstants.TEXT_MSG;private String msg;private String hostName;@Overridepublic void run(){if (GuQiuUtils.isOneNull(chatPanel)){return;}switch (msgType){// 文本消息case GuQiuConstants.TEXT_MSG:sendMessage();break;// 文件case GuQiuConstants.FILE_MSG:sendFile();break;// 表情case GuQiuConstants.FACE_MSG:break;// 声音case GuQiuConstants.SOUND_MSG:break;default:break;}}/** * <pre> * 发送普通消息 * </pre> * @param msg */private void sendMessage(){if (GuQiuUtils.isOneNull(hostName, msg)){return;}try{Socket socket = new Socket();InetAddress addr = InetAddress.getByName(hostName);InetSocketAddress endpoint = new InetSocketAddress(addr.getHostAddress(), GuQiuConstants.GUQIU_PORT);socket.connect(endpoint, 1000 * 60 * 5);OutputStream os = socket.getOutputStream();// 报头,1000为普通消息,前1024个字节为报头信息String title = MessageFormat.format(GuQiuConstants.TITLE_TEMPLATE, String.valueOf(GuQiuConstants.TEXT_MSG),GuQiuConstants.EMPTY_STRING, GuQiuConstants.EMPTY_STRING);byte[] sorceBytes = title.getBytes();byte[] titleBytes = new byte[1024];System.arraycopy(sorceBytes, 0, titleBytes, 0, sorceBytes.length);os.write(titleBytes);// 发完报头后发消息主体os.write(msg.getBytes());socket.shutdownOutput();socket.close();}catch (Exception e){chatPanel.append("消息发送失败,原因:" + e.getMessage() + '\n');chatPanel.setCaretPosition(chatPanel.getText().length());}}/** * <pre> * 发送文件 * </pre> * @param msg */private void sendFile(){try{fileLabel.setVisible(true);Socket socket = new Socket();InetAddress addr = InetAddress.getByName(hostName);InetSocketAddress endpoint = new InetSocketAddress(addr.getHostAddress(), GuQiuConstants.GUQIU_PORT);socket.connect(endpoint, 1000 * 60 * 5);OutputStream os = socket.getOutputStream();// 报头,1002为文件消息,前1024个字节为报头信息File file = new File(msg);long fileLength = file.length();String title = MessageFormat.format(GuQiuConstants.TITLE_TEMPLATE, String.valueOf(GuQiuConstants.FILE_MSG),file.getName(), fileLength);byte[] sorceBytes = title.getBytes();byte[] titleBytes = new byte[1024];System.arraycopy(sorceBytes, 0, titleBytes, 0, sorceBytes.length);os.write(titleBytes);// 发完报头后发消息主体fileLabel.setText(MessageFormat.format(GuQiuConstants.SEND_FILE_MSG, fileLength, 0));FileInputStream fis = new FileInputStream(file);byte[] buffer = new byte[2048 * 5];int readCount = 0;long tempCount = 0L;while ((readCount = fis.read(buffer)) != -1){os.write(buffer, 0, readCount);fileLabel.setText(MessageFormat.format(GuQiuConstants.SEND_FILE_MSG, fileLength,(tempCount + readCount)));}fis.close();socket.shutdownOutput();socket.close();fileLabel.setVisible(false);chatPanel.append("文件发送完成!\n");}catch (Exception e){chatPanel.append("文件发送失败,原因:" + e.getMessage() + '\n');}chatPanel.setCaretPosition(chatPanel.getText().length());}public JTextArea getChatPanel(){return chatPanel;}public void setChatPanel(JTextArea chatPanel){this.chatPanel = chatPanel;}public int getMsgType(){return msgType;}public void setMsgType(int msgType){this.msgType = msgType;}public String getMsg(){return msg;}public void setMsg(String msg){this.msg = msg;}public String getHostName(){return hostName;}public void setHostName(String hostName){this.hostName = hostName;}public JLabel getFileLabel(){return fileLabel;}public void setFileLabel(JLabel fileLabel){this.fileLabel = fileLabel;}}


2、消息接收线程,由于使用自定义协议,规定前1024个字节为报头,所以接收消息时也分两段接收:

/** * <pre> * Title: GuQiuServer.java * Project: GuQiu * Type:leoly.threads.GuQiuServer * Author:255507 * Create: 2011-9-7 上午11:17:09 * Copyright: Copyright (c) 2011 * Company: * <pre> */package leoly.threads;import java.io.BufferedReader;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.net.ServerSocket;import java.net.Socket;import java.net.UnknownHostException;import java.text.MessageFormat;import javax.swing.DefaultListModel;import javax.swing.JFileChooser;import javax.swing.JLabel;import javax.swing.JOptionPane;import javax.swing.JTextArea;import leoly.beans.ChatParamBean;import leoly.beans.MessageTitle;import leoly.frames.MainFrame;import leoly.utils.ChatFrameFinder;import leoly.utils.GuQiuConstants;import leoly.utils.GuQiuUtils;/** * <pre> * </pre> * @author 255507 * @version 1.0, 2011-9-7 */public class GuQiuServer extends Thread{private MainFrame parentFrame;@Overridepublic void run(){System.out.println("Create Server.");try{ServerSocket server = new ServerSocket(GuQiuConstants.GUQIU_PORT);while (true){Socket socket = server.accept();InputStream is = socket.getInputStream();// 读取前1024个字节作为信息报头,可能发送的信息为:文本,文件,文本和表情byte[] titleBytes = new byte[1024];is.read(titleBytes, 0, titleBytes.length);String title = new String(titleBytes);MessageTitle mt = MessageTitle.analyzeMsg(title);switch (mt.getMsgType()){case GuQiuConstants.VALIDATE_MSG:processValidate(socket);break;case GuQiuConstants.TEXT_MSG:processText(is, socket);break;case GuQiuConstants.FACE_MSG:break;case GuQiuConstants.FILE_MSG:processFile(is, socket, mt);break;case GuQiuConstants.SOUND_MSG:break;default:break;}socket.close();}}catch (UnknownHostException e){e.printStackTrace();}catch (IOException e){e.printStackTrace();}catch (Exception e){e.printStackTrace();}}/** * <pre> * 接收文件 * </pre> * @param is * @param socket * @param msgContent * @throws Exception */private void processFile(InputStream is, Socket socket, MessageTitle mt) throws Exception{String hostName = socket.getInetAddress().getHostName();int option = JOptionPane.showConfirmDialog(parentFrame, "收到来自:" + hostName + "的文件,需要接收吗?", "提示信息",JOptionPane.YES_NO_OPTION);if (option == JOptionPane.NO_OPTION){return;}ChatParamBean chatBean = getChatBean(hostName);JLabel fileLabel = chatBean.getFileLabel();JTextArea msgPanel = chatBean.getChatPanel();JFileChooser chooser = new JFileChooser();chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);int chooseOption = chooser.showSaveDialog(parentFrame);if (chooseOption == JFileChooser.APPROVE_OPTION){String selectPath = chooser.getSelectedFile().getAbsolutePath();if (GuQiuUtils.isEmpty(selectPath)){return;}fileLabel.setVisible(true);fileLabel.setText(MessageFormat.format(GuQiuConstants.RECIEVE_FILE_MSG, mt.getMsgLength(), 0));byte[] buffer = new byte[2048 * 5];FileOutputStream fos = new FileOutputStream(selectPath + "\\" + mt.getMsgContent());int readCount = 0;long tempCount = 0L;while ((readCount = is.read(buffer)) != -1){fos.write(buffer, 0, readCount);fileLabel.setText(MessageFormat.format(GuQiuConstants.RECIEVE_FILE_MSG, mt.getMsgLength(),(tempCount + readCount)));}fos.close();socket.shutdownInput();fileLabel.setVisible(false);msgPanel.append("文件接收完成!\n");msgPanel.setCaretPosition(msgPanel.getText().length());}}/** * <pre> * 接收消息 * </pre> * @param is * @param socket * @throws Exception */private void processText(InputStream is, Socket socket) throws Exception{BufferedReader reader = new BufferedReader(new InputStreamReader(is));String msg = null;StringBuffer sb = new StringBuffer();while ((msg = reader.readLine()) != null){sb.append(msg + "\n");}String hostName = socket.getInetAddress().getHostName();String profix = '[' + GuQiuUtils.getFormatDate() + "] " + hostName + " 说:\n";sb.insert(0, profix);ChatParamBean chatBean = getChatBean(hostName);JTextArea msgPanel = chatBean.getChatPanel();msgPanel.append(sb.toString());msgPanel.setCaretPosition(msgPanel.getText().length());socket.shutdownInput();}/** * <pre> * 处理验证信息 * </pre> * @param socket */private void processValidate(Socket socket){String hostName = socket.getInetAddress().getHostName();DefaultListModel listModel = ChatFrameFinder.getListModel();if (null != listModel && !listModel.contains(hostName)){listModel.addElement(hostName);}}/** * <pre> * 获取聊天窗口参数 * </pre> * @param hostName * @return */private ChatParamBean getChatBean(String hostName){ChatParamBean chatBean = ChatFrameFinder.getChatBean(hostName);if (GuQiuUtils.isOneNull(chatBean)){chatBean = parentFrame.createChatFrame(hostName);}return chatBean;}public MainFrame getParentFrame(){return parentFrame;}public void setParentFrame(MainFrame parentFrame){this.parentFrame = parentFrame;}}

3、还有一个扩散寻找局域网某个网段内在线用户的类:

/** * <pre> * Title: GuQiuScaner.java * Project: GuQiu * Type:leoly.threads.GuQiuScaner * Author:255507 * Create: 2011-9-7 上午10:58:22 * Copyright: Copyright (c) 2011 * Company: * <pre> */package leoly.threads;import javax.swing.DefaultListModel;import javax.swing.JProgressBar;import leoly.utils.GuQiuUtils;/** * <pre> * 以本机IP为中心,扩散寻找在线用户 * </pre> * @author 255507 * @version 1.0, 2011-9-7 */public class GuQiuScaner extends Thread{private DefaultListModel listModel;private JProgressBar progress;@Overridepublic void run(){if (GuQiuUtils.isOneNull(listModel, progress)){return;}String localIp = GuQiuUtils.getLocalHost();String ipProfix = localIp.substring(0, localIp.lastIndexOf('.') + 1);String tempIp = null;String checkResult = null;progress.setVisible(true);for (int i = 25; i <= 255; i++){tempIp = ipProfix + i;System.out.println(tempIp);checkResult = GuQiuUtils.checkConnected(tempIp);if (!GuQiuUtils.isEmpty(checkResult) && !listModel.contains(checkResult)){listModel.addElement(checkResult);}progress.setValue(i);}progress.setVisible(false);}public DefaultListModel getListModel(){return listModel;}public void setListModel(DefaultListModel listModel){this.listModel = listModel;}public JProgressBar getProgress(){return progress;}public void setProgress(JProgressBar progress){this.progress = progress;}}

4、静态常量类:

package leoly.utils;public interface GuQiuConstants{// 文本信息int TEXT_MSG = 1000;// 表情信息int FACE_MSG = 1001;// 文件信息int FILE_MSG = 1002;// 声音信息int SOUND_MSG = 1003;// 验证消息int VALIDATE_MSG = 1004;/** * 没联网的IP */String NO_LINKED = "127.0.0.1";/** * 古秋使用的端口 */int GUQIU_PORT = 5959;/** * 消息分隔符 */String TITLE_SEPORATOR = ";;;";/** * 空字符串 */String EMPTY_STRING = "";/** * 发送文件消息 */String SEND_FILE_MSG = "正在发送文件:大小[{0}],已完成[{1}]";/** * 接收文件消息 */String RECIEVE_FILE_MSG = "正在接收文件:大小[{0}],已完成[{1}]";/** * 报头模板 */String TITLE_TEMPLATE = "[{0};;;{1};;;{2}]";}


5、工具类:

/** * <pre> * Title: StringUtils.java * Project: GuQiu * Type:leoly.utils.StringUtils * Author:255507 * Create: 2011-9-7 上午09:08:05 * Copyright: Copyright (c) 2011 * Company: * <pre> */package leoly.utils;import java.io.OutputStream;import java.net.InetAddress;import java.net.InetSocketAddress;import java.net.Socket;import java.net.UnknownHostException;import java.text.MessageFormat;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.Collection;import java.util.List;import java.util.Map;/** * <pre> * </pre> * @author 255507 * @version 1.0, 2011-9-7 */public class GuQiuUtils{/** * <pre> *  * </pre> * @param testStr * @return */public static boolean isEmpty(String testStr){return (null == testStr || testStr.length() == 0 || "null".equalsIgnoreCase(testStr));}/** * <pre> *  * </pre> * @param testStr * @return */public static boolean isEmpty(Collection testStr){return (null == testStr || testStr.size() == 0);}/** * <pre> *  * </pre> * @param testStr * @return */public static boolean isEmpty(Map testStr){return (null == testStr || testStr.size() == 0);}/** * 参数列表中是否存在空对象 * @param objects 参数列表 * @return */public static boolean isOneNull(Object... objects){boolean result = false;if (null == objects){result = true;}else{String tempStr = null;for (Object tempObj : objects){if ((tempObj instanceof String)){tempStr = (String) tempObj;if (GuQiuUtils.isEmpty(tempStr)){result = true;break;}}else if (tempObj instanceof Collection){List obj = (List) tempObj;if (isEmpty(obj)){result = true;break;}}else if (tempObj instanceof Map){Map obj = (Map) tempObj;if (isEmpty(obj)){result = true;break;}}else if (null == tempObj){result = true;break;}}}return result;}/** * <pre> * 获取本机IP * </pre> * @return */public static String getLocalHost(){String ipStr = GuQiuConstants.NO_LINKED;try{InetAddress netAddr = InetAddress.getLocalHost();ipStr = netAddr.getHostAddress();}catch (UnknownHostException e){e.printStackTrace();}return ipStr;}/** * <pre> * 检查网络是否联通 * </pre> * @return */public static String checkConnected(String checkIp){String result = null;try{Socket socket = new Socket();InetSocketAddress addr = new InetSocketAddress(checkIp, GuQiuConstants.GUQIU_PORT);socket.connect(addr, 1200);if (socket.isConnected()){String msg = MessageFormat.format(GuQiuConstants.TITLE_TEMPLATE, String.valueOf(GuQiuConstants.VALIDATE_MSG),GuQiuConstants.EMPTY_STRING, GuQiuConstants.EMPTY_STRING);OutputStream os = socket.getOutputStream();os.write(msg.getBytes());result = addr.getHostName();socket.shutdownOutput();}socket.close();}catch (Exception e){}return result;}/** * <pre> * 获取格式化后的当前时间 * </pre> * @return */public static String getFormatDate(){SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.format(Calendar.getInstance().getTime());}}

终于贴完核心代码了,大家如果需要全部的Eclipse工程源码,请到资源区下载吧:

点击打开链接


代码只供参考,还存在很多问题,比如扩散寻找在线用户就很慢,需要大约十分钟左右才完成,不过总体可以使用了。

原创粉丝点击