现在的位置: 首页 > 综合 > 正文

基于socket的android聊天工具简单实现

2015年02月10日 ⁄ 综合 ⁄ 共 9053字 ⁄ 字号 评论关闭

大家都知道,一个聊天工具需要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级别。

看看效果吧!

启动服务器:

在两个模拟器上登录:

发几个消息看看:

试试图片:


抱歉!评论已关闭.