大家都知道,一个聊天工具需要client和server两部分, server的部分负责处理用户登录等,最主要的还是要负责转发client的消息, 我在刚开始做的时候也是用的类似“send:from:to:message”这样的命令,后来我想给软件加上发送图片的功能,这样的方式显然不合适,而且我更想软件的可扩展性强,接下来我就分析了一下几个命令:
登录:login:username:password
发送文本:text:from:to:message
发送图片:image:from:to:image
好像有点眉目了, 能不能把它们封装成对象用对象序列化的方式传送呢? 如果可以,该怎么设计类可扩展性强呢?
后来我想让所有的命令都具有相同的基类,显然这几个命令的第一段都是一个flag, 那我先做一个基类,在基类中设置一个flag,表示是什么命令:
// 消息的基类 public abstract class BaseCommand implements Serializable { protected String mFlag; public void setFlag(String flag) { mFlag = flag; } public String getFlag() { return mFlag; } }
接下来就是实现登录命令了,登录毕竟是所有聊天的前提嘛:
// 登录消息 login:username:password public class LoginCommand extends BaseCommand { private String mUsername; private String mPassword; public LoginCommand(String userName, String password) { mFlag = "login"; mUsername = userName; mPassword = password; } public String getUserName() { return mUsername; } public String getPassword() { return mPassword; } }
在登录的构造方法中强制设置flag为login,代表这条命令是登录。
接下就是封装消息命令了,目前我们考虑的消息有文本和图片,刚开始,我想着在做一个基类继承自BaseCommand,然后不同的消息不同处理,后来发现这种做法不仅是脱了裤子放屁,而且可扩展性也没那么好,这时我想到了Object,既然Object是万物的老祖宗,那我何不把所有的消息看成Object呢,服务器也不同去解析他,只需要客户端通过flag将Object强制转化成不同类型(String, byte[])就行,那就看看消息的封装类:
public class MessageWrapper extends BaseCommand { protected String mSender; protected String mReceiver; protected Object mMessage; public MessageWrapper(String flag, String sender, String receiver, Object data) { mFlag = flag; mSender = sender; mReceiver = receiver; mMessage = data; } public String getSender() { return mSender; } public String getReceiver() { return mReceiver; } public Object getMessage() { return mMessage; } }
其实也简单,只不过flag需要我们在构造方法中指定。
那我们的服务器该怎么处理呢?来看服务器类:
public class Server implements Runnable { private Socket mSocket; private Onlines mOnlines; public Server(Socket socket) { mSocket = socket; mOnlines = Onlines.getInstance(); } @Override public void run() { try { ObjectInputStream in = new ObjectInputStream(mSocket.getInputStream()); BaseCommand baseCmd = (BaseCommand) in.readObject(); if("login".equals(baseCmd.getFlag())) { LoginCommand loginCmd = (LoginCommand) baseCmd; System.out.println(loginCmd.getUserName() + " login..."); PrintWriter writer = new PrintWriter(mSocket.getOutputStream()); String userName = loginCmd.getUserName(); String pwd = loginCmd.getPassword(); // 用户名和密码相同 if(userName.equals(pwd)) { writer.println("success"); mOnlines.put(userName, mSocket); }else { writer.println("error"); } writer.flush(); }else { MessageWrapper textMsg = (MessageWrapper) baseCmd; System.out.println(textMsg.getSender() + " send to " + textMsg.getReceiver()); Socket recSocket = mOnlines.get(textMsg.getReceiver()); ObjectOutputStream writer = new ObjectOutputStream(recSocket.getOutputStream()); writer.writeObject(textMsg); writer.flush(); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { boolean flag = true; ServerSocket server = new ServerSocket(8888); ExecutorService threadPool = Executors.newCachedThreadPool(); while(flag) { threadPool.execute(new Server(server.accept())); System.out.println("linked in ..."); } threadPool.shutdownNow(); server.close(); } }
所有的消息我们在一个else中处理了,而且不需要知道这个消息的类型,这消息又不是发给服务器的, 管那么多干嘛,知道是发给谁的,直接转发就好了,让客户端自己处理去吧!
对了,这里还有一个Onlines类,是保存了所有在线的用户,因为当有消息发上来的时候我们需要通知这个用户嘛:
public class Onlines { private static Onlines sOnlines; private LinkedHashMap<String, Socket> mOnlineUsers = new LinkedHashMap<String, Socket>(); private Onlines() { } public synchronized static Onlines getInstance() { if(null == sOnlines) { sOnlines = new Onlines(); } return sOnlines; } public synchronized void put(String key, Socket socket) { if(!mOnlineUsers.containsKey(key)) { mOnlineUsers.put(key, socket); } } public Socket get(String key) { if(mOnlineUsers.containsKey(key)) { return mOnlineUsers.get(key); } return null; } public synchronized void remove(String key) { if(mOnlineUsers.containsKey(key)) { mOnlineUsers.remove(key); } } }
到这里,服务器端我们就基本完成了,是不是挺简单!就这么几行代码!
接下来看看客户端吧,android客户端的布局很简单, 就不发布局文件的代码了, 只关注java代码。
既然是传送序列化的对象,那么在android客户端中我们也要有那几个消息类,而且包名也必须相同!android客户端的功能就是用同一个类封装不同的消息通过socket发送给服务器。
先看看登录吧:
public class MainActivity extends Activity { private EditText mUsername, mPassword; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); } private void initViews() { mUsername = (EditText) findViewById(R.id.login_user); mPassword = (EditText) findViewById(R.id.login_pwd); } public void login(View view) { final String userName = mUsername.getText().toString().trim(); final String pwd = mPassword.getText().toString().trim(); final LoginCommand loginCmd = new LoginCommand(userName, pwd); Login login = new Login(); login.setOnLoginListener(new Login.OnLoginListener() { @Override public void onLogin(boolean success) { if(success) { Toast.makeText(MainActivity.this, "登录成功", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(MainActivity.this, Chat.class); intent.putExtra("user", userName); startActivity(intent); finish(); }else { Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_SHORT).show(); } } }); login.login(loginCmd); } }
自定义了一个Login类负责发送消息:
<pre name="code" class="java">public class Chat extends Activity { private String mUser; private ListView mMessages; private EditText mEditMessage; private List<Map<String, Object>> mData = new ArrayList<Map<String, Object>>(); private MessageAdapter mAdapter; private SendMessage mSender; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.chat_layout); mUser = getIntent().getStringExtra("user"); mMessages = (ListView) findViewById(R.id.messages); mEditMessage = (EditText) findViewById(R.id.msg); mSender = new SendMessage(); mSender.setOnSendListener(new SendListener()); initAdapters(); initReceiver(); } private void initReceiver() { ReceiveMessage rm = new ReceiveMessage(); rm.setOnReceiveListener(new ReceiveMessage.OnReceiveListener() { @Override public void onReceive(MessageWrapper msg) { notifyDataSetChanged(msg.getFlag(), msg.getSender(), msg.getMessage()); } }); rm.run(); } private void initAdapters() { mAdapter = new MessageAdapter(this, mData); mMessages.setAdapter(mAdapter); } public void send(View view) { String[] msg = mEditMessage.getText().toString().trim().split(":"); notifyDataSetChanged("text", "我", msg[1]); MessageWrapper wrapper = new MessageWrapper("text", mUser, msg[0], msg[1]); mSender.send(wrapper); } private void notifyDataSetChanged(String flag, String from, Object msg) { Map<String, Object> temp = new HashMap<String, Object>(); temp.put("flag", flag); temp.put("user", from); temp.put("message", msg); mData.add(temp); mAdapter.notifyDataSetChanged(); } public void sendImage(View view) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); startActivityForResult(intent, 1); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(RESULT_OK == resultCode) { Uri uri = data.getData(); ContentResolver resolver = getContentResolver(); try { // 获取输入流 InputStream in = resolver.openInputStream(uri); byte[] by = StreamUtils.stream2Byte(in); notifyDataSetChanged("image", "我", by); MessageWrapper wrapper = new MessageWrapper("image", mUser, mEditMessage.getText().toString().trim(), by); mSender.send(wrapper); } catch (Exception e) { e.printStackTrace(); } } super.onActivityResult(requestCode, resultCode, data); } private class SendListener implements SendMessage.onSendListener { @Override public void onSend(MessageWrapper msg) { Toast.makeText(Chat.this, msg.getMessage().toString(), Toast.LENGTH_SHORT).show(); } } }
MessageWrapper wrapper = new MessageWrapper("text", mUser, msg[0], msg[1]); 是封装一个普通的文本消息
<pre name="code" class="java">MessageWrapper wrapper = new MessageWrapper("image", mUser, mEditMessage.getText().toString().trim(), by); 是封装一个图片消息
发送的方式都是一样的,将来要想加入语音功能,也很简单了!
先看看发送消息的处理吧:
public class SendMessage { public onSendListener mListener; public void setOnSendListener(onSendListener listener) { mListener = listener; } private Handler mHandler = new Handler() { public void handleMessage(Message msg) { if(Constant.SUCCESS == msg.what) { mListener.onSend((MessageWrapper) msg.obj); } }; }; public void send(final MessageWrapper msg) { new Thread(new Runnable() { @Override public void run() { try { Socket socket = new Socket(); socket.connect(new InetSocketAddress(Constant.SVR_IP, Constant.SVR_PORT), 4000); ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(msg); oos.flush(); Message m = mHandler.obtainMessage(Constant.SUCCESS, msg); m.sendToTarget(); } catch (Exception e) { e.printStackTrace(); } } }).start(); } public interface onSendListener { public void onSend(MessageWrapper msg); } }
这个类可以发送任何形式的消息,只要你用MessageWrapper包装一下!
接受消息呢?
public class ReceiveMessage { public OnReceiveListener mListener; public void setOnReceiveListener(OnReceiveListener listener) { mListener = listener; } private Handler mHandler = new Handler() { public void handleMessage(Message msg) { if(Constant.SUCCESS == msg.what) { mListener.onReceive((MessageWrapper) msg.obj); } } }; public void run() { new Thread(new Runnable() { @Override public void run() { try { for(;;) { System.out.println("waiting for message..."); ObjectInputStream ois = new ObjectInputStream(PConnection.socket.getInputStream()); MessageWrapper message = (MessageWrapper) ois.readObject(); Message msg = mHandler.obtainMessage(Constant.SUCCESS, message); msg.sendToTarget(); } } catch (Exception e) { e.printStackTrace(); } } }).start(); } public interface OnReceiveListener { public void onReceive(MessageWrapper msg); } }
用了一个死循环,不过也没关系,因为在没有消息到达的时候PConnection.socket.getInputStream()是阻塞的。 对了,PConnection.socket.getInputStream()这里是使用的登录时保存的socket,不信可以回头看看登录的处理, 我们保存了一个长连接。 这里我还没想到什么好的方式替代这种长连接!
至此,我们一个简单的socket聊天程序就完成了,当然,这仅仅出于demo级别。
看看效果吧!
启动服务器:
在两个模拟器上登录:
发几个消息看看:
试试图片: