内容摘要
1.程序架构
2.通信协议
3.服务器源代码
4.客户端源代码
5.运行效果
一、程序架构
在开发一个聊天室程序时,我们可以使用Socket、Remoting、WCF这些具有双向通信的协议或框架。而现在,我正要实现一个C#语言作为服务器端、Android作为客户端的聊天室。由于服务器端和客户端不是同一语言(C#和java),所有我选择了Socket作为通信协议。
图1.1所示,我们可以看出:android手机客户端A向服务器端发送消息,服务器端收到消息后,又把消息推送到android手机客户端B。
图1.1
二、通信协议
我们知道,在C#语言中使用Socket技术需要“四部曲”,即“Bind”,“Listen”,“Accept”,“Receive”。然而Socket编程不像WCF那样面向对象。而且对应每个请求都用同一种方式处理。作为习惯面向对象编程的我来说,编写一个传统的Socket程序很不爽。绞尽脑汁,我们将数据传输的格式改为json(JavaScript Object Notation 是一种轻量级的数据交换格式),面对对象的问题就解决了。
假设程序的服务契约有两个方法:“登陆”和“发送消息”。调用登陆的方法,就传送方法名(Method Name)为“Logon”的json数据;调用发送消息的方法,就传送方法名为“Send”的json数据。返回的数据中也使用json格式,这样在android客户端中也能知道是哪个方法的返回值了。
三、服务器源代码
首先需要编写一个处理客户端消息的接口:IResponseManager。
public interface IResponseManager { void Write(Socket sender, IList<Socket> cliens, IDictionary<string, object> param); }
其次,我们知道,换了是WCF编程的话,就需要在服务契约中写两个方法:“登陆”和“发送消息”。由于这里是Socket编程,我们实现之前写的IResponseManager接口,一个实现作为“登陆”的方法,另一个实现作为“发送消息”的方法。
public class LogonResponseManager : IResponseManager { public void Write(System.Net.Sockets.Socket sender, IList<System.Net.Sockets.Socket> cliens, IDictionary<string, object> param) { Console.WriteLine("客户端({0})登陆", sender.Handle); var response = new SocketResponse { Method = "Logon", DateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), Result = new { UserName = param["UserName"].ToString() } }; JavaScriptSerializer jss = new JavaScriptSerializer(); string context = jss.Serialize(response); Console.WriteLine("登陆发送的数据为:{0}", context); sender.Send(Encoding.UTF8.GetBytes(context + "\n")); } }
public class SendResponseManager : IResponseManager { public void Write(System.Net.Sockets.Socket sender, IList<System.Net.Sockets.Socket> cliens, IDictionary<string, object> param) { Console.WriteLine("客户端({0})发送消息", sender.Handle); var msgList = param["Message"] as IEnumerable<object>; if (msgList == null) { return; } var response = new SocketResponse { Method = "Send", DateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), Result = new { UserName = param["UserName"].ToString(), Message = msgList.Select(s => s.ToString()).ToArray() } }; JavaScriptSerializer jss = new JavaScriptSerializer(); string context = jss.Serialize(response); Console.WriteLine("消息发送的数据为:{0}", context); Parallel.ForEach(cliens, (item) => { try { item.Send(Encoding.UTF8.GetBytes(context + "\n")); } catch { }; }); } }
最后在Socket程序中使用反射加“策略模式”调用这两个接口实现类。
var typeName = "SocketServer." + request.Method + "ResponseManager, SocketServer"; Console.WriteLine("反射类名为:" + typeName); Type type = Type.GetType(typeName); if (type == null) { return; } var manager = Activator.CreateInstance(type) as IResponseManager; manager.Write(sender, this.socketClientSesson.Select(s => s.Key).ToList(), request.Param as IDictionary<string, object>);
完整的Socket服务器代码如下:
public class SocketHost { private IDictionary<Socket, byte[]> socketClientSesson = new Dictionary<Socket, byte[]>(); public int Port { get; set; } public void Start() { var socketThread = new Thread(() => { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint iep = new IPEndPoint(IPAddress.Any, this.Port); //绑定到通道上 socket.Bind(iep); //侦听 socket.Listen(6); //通过异步来处理 socket.BeginAccept(new AsyncCallback(Accept), socket); }); socketThread.Start(); Console.WriteLine("服务器已启动"); } private void Accept(IAsyncResult ia) { Socket socket = ia.AsyncState as Socket; var client = socket.EndAccept(ia); socket.BeginAccept(new AsyncCallback(Accept), socket); byte[] buf = new byte[1024]; this.socketClientSesson.Add(client, buf); try { client.BeginReceive(buf, 0, buf.Length, SocketFlags.None, new AsyncCallback(Receive), client); string sessionId = client.Handle.ToString(); Console.WriteLine("客户端({0})已连接", sessionId); } catch (Exception ex) { Console.WriteLine("监听请求时出错:\r\n" + ex.ToString()); } } private void Receive(IAsyncResult ia) { var client = ia.AsyncState as Socket; if (client == null || !this.socketClientSesson.ContainsKey(client)) { return; } int count = client.EndReceive(ia); byte[] buf = this.socketClientSesson[client]; if (count > 0) { try { client.BeginReceive(buf, 0, buf.Length, SocketFlags.None, new AsyncCallback(Receive), client); string context = Encoding.UTF8.GetString(buf, 0, count); Console.WriteLine("接收的数据为:", context); this.Response(client, context); } catch (Exception ex) { Console.WriteLine("接收的数据出错:\r\n{0}", ex.ToString()); } } else { try { string sessionId = client.Handle.ToString(); client.Disconnect(true); this.socketClientSesson.Remove(client); Console.WriteLine("客户端({0})已断开", sessionId); } catch (Exception ex) { Console.WriteLine("客户端已断开出错" + ex.ToString()); } } } private void Response(Socket sender, string context) { SocketRequest request = null; JavaScriptSerializer jss = new JavaScriptSerializer(); request = jss.Deserialize(context, typeof(SocketRequest)) as SocketRequest; if (request == null) { return; } var typeName = "SocketServer." + request.Method + "ResponseManager, SocketServer"; Console.WriteLine("反射类名为:" + typeName); Type type = Type.GetType(typeName); if (type == null) { return; } var manager = Activator.CreateInstance(type) as IResponseManager; manager.Write(sender, this.socketClientSesson.Select(s => s.Key).ToList(), request.Param as IDictionary<string, object>); } }
最后,json数据传输的实体对象为:
[Serializable] public class SocketRequest { public string Method { get; set; } public string DateTime { get; set; } public object Param { get; set; } }
[Serializable] public class SocketResponse { public string Method { get; set; } public string DateTime { get; set; } public object Result { get; set; } }
四、客户端源代码
1.布局文件
logon.xml:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/background"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="60dip" android:background="@drawable/logon" /> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:paddingLeft="10dp" android:paddingRight="10dp"> <View android:layout_width="fill_parent" android:layout_height="20dip" /> <TextView android:id="@+id/feedback_title" android:textColor="#FFFFFF" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="用户名:" /> <EditText android:id="@+id/edtUserName" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <View android:layout_width="fill_parent" android:layout_height="2dip" android:background="#FF909090" /> <TextView android:layout_width="fill_parent" android:textColor="#FFFFFF" android:layout_height="wrap_content" android:text="IP地址:" /> <EditText android:id="@+id/edtIp" android:layout_width="fill_parent" android:layout_height="wrap_content" android:digits="1234567890." android:text="192.168.1.101"/> <View android:layout_width="fill_parent" android:layout_height="2dip" android:background="#FF909090" /> <TextView android:layout_width="fill_parent" android:textColor="#FFFFFF" android:layout_height="wrap_content" android:text="端口号:" /> <EditText android:id="@+id/edtPort" android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="number" android:numeric="integer" android:text="1234"/> <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp"> </LinearLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent"> <View android:id="@+id/feedback_content" android:layout_width="fill_parent" android:layout_height="fill_parent" android:maxEms="10" android:minEms="10" android:gravity="top" android:layout_marginBottom="50dip" /> <Button android:id="@+id/btnLogon" android:layout_width="fill_parent" android:layout_height="50dp" android:text="登陆" android:textSize="19dp" android:layout_gravity="center_horizontal" android:layout_alignParentBottom="true" /> </RelativeLayout> </LinearLayout></LinearLayout>
main.xml:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/background"> <ListView android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/ltvMessage"> </ListView> <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content"> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/edtMessage" android:hint="请输入消息" android:layout_alignTop="@+id/btnSend" android:layout_toLeftOf="@+id/btnSend" /> <Button android:text="SEND" android:id="@+id/btnSend" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_alignParentRight="true" /> </RelativeLayout></LinearLayout>
listview_item.xml:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingBottom="3dip" android:paddingLeft="10dip"> <TextView android:layout_height="wrap_content" android:layout_width="fill_parent" android:id="@+id/itmMessage" android:textSize="20dip"> </TextView> <LinearLayout android:layout_width="fill_parent" android:orientation="horizontal" android:layout_height="20dip"> <TextView android:layout_height="fill_parent" android:layout_width="100dip" android:id="@+id/itmUserName" > </TextView> <TextView android:layout_height="fill_parent" android:layout_width="200dip" android:id="@+id/itmTime" > </TextView> </LinearLayout></LinearLayout>
SocketClient:
package ld.socket;import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.Socket;import java.net.UnknownHostException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.HashMap;import java.util.Map;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import android.os.Handler;import android.os.Message;import android.util.Log;public class SocketClient { private static Socket client; private static SocketClient instance = null; public static SocketClient getInstance() { if (instance == null) { synchronized (ChartInfo.class) { if (instance == null) { try { ChartInfo chartInfo = ChartInfo.getInstance(); client = new Socket(chartInfo.getIp(), chartInfo .getPort()); instance = new SocketClient(); } catch (UnknownHostException e) { // TODO Auto-generated catch block } catch (IOException e) { // TODO Auto-generated catch block } } } } return instance; } private SocketClient() { this.initMap(); this.startThread(); } private void initMap() { this.handlerMap = new HashMap<String, Handler>(); } public void close() { try { client.close(); } catch (IOException e) { // TODO Auto-generated catch block //e.printStackTrace(); } instance = null; } private void startThread() { Thread thread = new Thread() { @Override public void run() { while (true) { if (client == null || !client.isConnected()) { continue; } BufferedReader reader; try { reader = new BufferedReader(new InputStreamReader( client.getInputStream())); String line = reader.readLine(); Log.d("initSocket", "line:" + line); if (line.equals("")) { continue; } JSONObject json = new JSONObject(line); String method = json.getString("Method"); Log.d("initSocket", "method:" + method); if (method.equals("") || !handlerMap.containsKey(method)) { Log.d("initSocket", "handlerMap not method"); continue; } Handler handler = handlerMap.get(method); if (handler == null) { Log.d("initSocket", "handler is null"); continue; } Log.d("initSocket", "handler:" + method); Object obj = json.getJSONObject("Result"); Log.d("initSocket", "Result:" + obj); Message msg = new Message(); msg.obj = obj; handler.sendMessage(msg); } catch (IOException e) { } catch (JSONException e) { } } } }; thread.start(); } private Map<String, Handler> handlerMap; public void putHandler(String methodnName, Handler handler) { this.removeHandler(methodnName); this.handlerMap.put(methodnName, handler); } public void removeHandler(String methodnName) { if (this.handlerMap.containsKey(methodnName)) { this.handlerMap.remove(methodnName); } } public void logon(String userName) { Log.d("initSocket", "logon"); try { OutputStreamWriter osw = new OutputStreamWriter(client .getOutputStream()); BufferedWriter writer = new BufferedWriter(osw); JSONObject param = new JSONObject(); param.put("UserName", userName.replace("\n", " ")); JSONObject json = this.getJSONData("Logon", param); writer.write(json.toString()); writer.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void sendMessage(String message) { Log.d("initSocket", "Send"); try { OutputStreamWriter osw = new OutputStreamWriter(client .getOutputStream()); BufferedWriter writer = new BufferedWriter(osw); JSONArray array = new JSONArray(); for (String item : message.split("\n")) { array.put(item); } JSONObject param = new JSONObject(); param.put("Message", array); param.put("UserName", ChartInfo.getInstance().getUserName()); JSONObject json = this.getJSONData("Send", param); writer.write(json.toString()); writer.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private JSONObject getJSONData(String methodName, JSONObject param) { JSONObject json = new JSONObject(); try { json.put("Method", methodName); SimpleDateFormat format = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); json.put("DateTime", format.format(new Date())); json.put("Param", param); return json; } catch (JSONException e) { return null; } }}
LogonActivity:
package ld.socket;import org.json.JSONException;import org.json.JSONObject;import android.app.Activity;import android.app.AlertDialog;import android.content.ComponentName;import android.content.DialogInterface;import android.content.Intent;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.util.Log;import android.view.KeyEvent;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.EditText;public class LogonActivity extends Activity { private EditText edtUserName; private EditText edtIp; private EditText edtPort; private Button btnLogon; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.logon); this.initViews(); } private void initViews() { this.edtUserName = (EditText) this.findViewById(R.id.edtUserName); this.edtIp = (EditText) this.findViewById(R.id.edtIp); this.edtPort = (EditText) this.findViewById(R.id.edtPort); this.btnLogon = (Button) this.findViewById(R.id.btnLogon); this.btnLogon.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub // showAlert(edtUserName.getText().toString()); if (edtUserName.getText().toString().equals("")) { showDialog("请输入用户名"); return; } if (edtIp.getText().toString().equals("")) { showDialog("请输入IP地址"); return; } if (edtPort.getText().toString().equals("")) { showDialog("请输入端口号"); return; } int port = Integer.parseInt(edtPort.getText().toString()); ChartInfo chartInfo = ChartInfo.getInstance(); chartInfo.setIp(edtIp.getText().toString()); chartInfo.setPort(port); SocketClient proxy = SocketClient.getInstance(); if (proxy == null) { showDialog("未接入互联网"); setWireless(); return; } proxy.putHandler("Logon", new Handler() { @Override public void handleMessage(Message msg) { SocketClient proxy = SocketClient.getInstance(); proxy.removeHandler("Logon"); Log.d("initSocket", "handleMessage"); if (msg == null || msg.obj == null) { return; } JSONObject json = (JSONObject) msg.obj; try { String userName = json.getString("UserName"); Log.d("initSocket", "userName:" + userName); ChartInfo.getInstance().setUserName(userName); Intent itt = new Intent(); itt .setClass(LogonActivity.this, MainActivity.class); LogonActivity.this.startActivity(itt); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); proxy.logon(edtUserName.getText().toString()); } }); } private void setWireless() { Intent mIntent = new Intent("/"); ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.WirelessSettings"); mIntent.setComponent(comp); mIntent.setAction("android.intent.action.VIEW"); startActivityForResult(mIntent, 0); } private void showDialog(String mess) { new AlertDialog.Builder(this).setTitle("信息").setMessage(mess) .setNegativeButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } }).show(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { AlertDialog alertDialog = new AlertDialog.Builder( LogonActivity.this).setTitle("退出程序").setMessage("是否退出程序") .setPositiveButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { LogonActivity.this.finish(); } }).setNegativeButton("取消", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { return; } }).create(); // 创建对话框 alertDialog.show(); // 显示对话框 return false; } return false; } @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); SocketClient proxy = SocketClient.getInstance(); if (proxy != null) { proxy.close(); } }}
MainActivity:
package ld.socket;import org.json.JSONException;import org.json.JSONObject;import android.app.Activity;import android.app.AlertDialog;import android.content.DialogInterface;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.util.Log;import android.view.KeyEvent;import android.view.View;import android.view.WindowManager;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.EditText;import android.widget.ListView;public class MainActivity extends Activity { private EditText edtMessage; private Button btnSend; private ListView ltvMessage; private MessageAdapter adapter; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 隐藏键盘 this.getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); Log.d("initSocket", "MessageAdapter"); this.adapter = new MessageAdapter(this); Log.d("initSocket", "adapter is ok"); this.findThisViews(); this.initHandler(); this.serOnClick(); Log.d("initSocket", "onCreate"); } private void findThisViews() { this.edtMessage = (EditText) this.findViewById(R.id.edtMessage); this.btnSend = (Button) this.findViewById(R.id.btnSend); this.ltvMessage = (ListView) this.findViewById(R.id.ltvMessage); // this.ltvMessage.setEnabled(false); this.ltvMessage.setAdapter(this.adapter); } private void initHandler() { Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.obj == null) { Log.d("initSocket", "handleMessage is null"); return; } Log.d("initSocket", "handleMessage"); try { JSONObject json = (JSONObject) msg.obj; String userName = json.getString("UserName"); StringBuilder sb = new StringBuilder(); int length = json.getJSONArray("Message").length(); for (int i = 0; i < length; i++) { String item = json.getJSONArray("Message").getString(i); if (item.equals("")) { continue; } if (length > i + 1) { Log.d("initSocket", "length:" + length); Log.d("initSocket", "i:" + i); Log.d("initSocket", "item:" + item); item += "\n"; } sb.append(item); } MessageRecord record = new MessageRecord(); record.setUserName(userName); record.setMessage(sb.toString()); MainActivity.this.adapter.add(record); adapter.notifyDataSetChanged(); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; SocketClient proxy = SocketClient.getInstance(); proxy.putHandler("Send", handler); } private void serOnClick() { this.btnSend.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub btnSend.setEnabled(false); String txt = edtMessage.getText().toString(); if (txt.equals("")) { btnSend.setEnabled(true); return; } SocketClient proxy = SocketClient.getInstance(); proxy.sendMessage(txt); edtMessage.setText(""); btnSend.setEnabled(true); } }); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { AlertDialog alertDialog = new AlertDialog.Builder( MainActivity.this).setTitle("询问").setMessage("是否注销登录?") .setPositiveButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { MainActivity.this.finish(); } }).setNegativeButton("取消", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { return; } }).create(); // 创建对话框 alertDialog.show(); // 显示对话框 return false; } return false; }}
五、运行效果