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

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

大家都知道,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;

	@Override
	public 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;

	@Override
	public 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;

	@Override
	public 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工程源码,请到资源区下载吧:

点击打开链接


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

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值