Socket 也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应网络的传输控制层中的 TCP 和 UDP 协议,TCP 协议是面向连接的协议,提供稳定的双向功能,TCP 链接的建立经过“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而 UDP 是无连接的,提供不稳定的单向连接功能,当然 UDP 也可以实现双向通信功能。在性能上,UDP 具有更好的效率,其缺点是不能保证数据一定能正确传输。尤其是在网络拥堵的情况下。接下来我们演示一个跨进程聊天程序,两个进程可以通过 Socket 来实现信息的传输,Socket 本身可以支持传输任意字节流的。
使用 Socket 来进行通信,有两点需要注意,首先需要声明权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
其次要注意不鞥在主线程中访问网络。这样会导致无法再 Android 4.0 及其以上的设备中运行,会抛出如下异常:android.os.NetwworkOnMainThreadException。下面开始我们的聊天程序,比较简单,首先在远程 Socket 建立一个 TCP 服务,然后在主界面中链接 TCP 服务,连接上了以后,就可以给服务端发消息。对于我们发送的每一条文本信息,服务端都会随机的回应我们一句话。为了更好的展示 Socket 的工作机制,在服务端我们需要处理下,使其能够和多个客户端同时建立链接并相应。
先看一下服务端设计,当 Service 启动时,会在线程中建立 TCP 服务,这里监听的是 8688 端口,然后就可以等待客户端的连接请求。当客户端连接时,就会生成一个新的 Socket,通过每次新创建的 Socket 就可以分别和不同的客户端通信了。服务端每收到一次客户端的信息就会随机回复一句话给客户端。当客户端连接断开时,服务端也会相应的关闭对应的 Socket 并结束通话线程,这点如何做到呢?方法有很多,这里是通过判断服务端输入的流的返回值来确定的,当客户端断开连接后,服务端这边的输入流会返回 null,这个时候就知道客户端退处理,服务端代码如下:
public class TCPServerService extends Service {
public static final String TAG = "TCPServerService";
private boolean mIsServiceDestoryed = true;
private String [] mDefiendMessages = new String[]{"你好啊,哈哈", "请问你叫什么名字啊?", "今天北京天气不错啊,shy", "你知道吗,我可是可以和多个人同事聊天的哦", "给你讲个笑话吧"};
@Override
public void onCreate() {
super.onCreate();
new Thread(new TCPServer()).start();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
mIsServiceDestoryed = false;
}
private class TCPServer implements Runnable {
@Override
public void run() {
ServerSocket serverSocket = null;
try {
//监听 8688 接口
serverSocket = new ServerSocket(8688);
} catch (IOException e) {
System.err.println("establish tcp server filled, port: 8688");
e.printStackTrace();
return;
}
while (mIsServiceDestoryed) {
try {
//接受客户端请求
final Socket client = serverSocket.accept();
new Thread() {
@Override
public void run() {
try {
responseClient(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void responseClient(Socket client) throws IOException {
//用于接收客户端消息
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
//用于向客户端发送消息
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
out.println("欢迎来到聊天室!");
while (mIsServiceDestoryed){
String str = in.readLine();
Log.i(TAG, "msg from client:" + str);
if(str == null) {
//客户端断开连接
break;
}
int i = new Random().nextInt(mDefiendMessages.length);
String msg = mDefiendMessages[i];
out.println(msg);
Log.i(TAG, "send msg:" + msg);
}
Log.i(TAG, "client quit.");
close(out);
close(in);
client.close();
}
}
public void close(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
接着看一下客户端,客户端 Activity 启动时,会在 onCreate 中开启一个线程去链接服务端的 Socket,至于为什么用线程在前边已经做了介绍。为了确定能够连接成功,这里采用了超时重连的策略,每次连接失败后都会重新尝试建立连接,当然为了降低重试机制的开销,我们加入了休眠机制,即每次重试的时间间隔是 1000 毫秒。
Socket socket = null;
while (socket == null) {
try {
socket = new Socket("localhost", 8688);
mClientSocket = socket;
mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED);
Log.i(TAG, "connect server success.");
} catch (IOException e) {
e.printStackTrace();
SystemClock.sleep(1000);
Log.i(TAG, "connect tcp server failed, retry...");
}
}
服务端连接成功后,就可以和服务端进行通信了。下面的代码在线程中通过 while 循环不断地从服务端获取数据,同时在 Activity 退出时,就退出循环并终止线程。
//接收服务端消息
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (!SocketActivity.this.isFinishing()){
String msg = br.readLine();
Log.e(TAG, "receive msg:" + msg);
if(null != msg) {
String time = new SimpleDateFormat("HH:mm:ss").format(new Date(System.currentTimeMillis()));
final String showMsg = "server" + time + ":" + msg + "\n";
mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG, showMsg).sendToTarget();
}
}
close(mPrintWriter);
close(br);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
同时,在 Activity 退出时,还要关闭当前的 Socket。如下所示:
@Override
protected void onDestroy() {
super.onDestroy();
if(mClientSocket != null) {
try {
mClientSocket.shutdownInput();
mClientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
接着和发送消息的过程,这个就简单了,这里不再详细说明,客户端完整的代码如下:
public class SocketActivity extends Activity implements View.OnClickListener {
private static final String TAG = "SocketActivity";
private Socket mClientSocket;
private PrintWriter mPrintWriter;
private static final int MESSAGE_RECEIVE_NEW_MSG = 1;
private static final int MESSAGE_SOCKET_CONNECTED = 2;
private Button mSendButton;
private TextView mMessageTextView;
private EditText mMessageEditText;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_RECEIVE_NEW_MSG:
mMessageTextView.setText(mMessageTextView.getText().toString() + msg.obj);
break;
case MESSAGE_SOCKET_CONNECTED:
mSendButton.setEnabled(true);
break;
default:
break;
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
mMessageTextView = (TextView) findViewById(R.id.msg_container);
mSendButton = (Button) findViewById(R.id.send);
mSendButton.setOnClickListener(this);
mMessageEditText = (EditText) findViewById(R.id.msg);
startService(new Intent(this, TCPServerService.class));
new Thread(new Runnable() {
@Override
public void run() {
connectTCPServer();
}
}).start();
}
@Override
public void onClick(View v) {
final String msg = mMessageEditText.getText().toString();
if(!TextUtils.isEmpty(msg) && mPrintWriter != null) {
mPrintWriter.println(msg);
mMessageEditText.setText("");
String time = new SimpleDateFormat("HH:mm:ss").format(new Date(System.currentTimeMillis()));
final String showMsg = "self" + time + ":" + msg + "\n";
mMessageTextView.setText(mMessageTextView.getText().toString() + showMsg);
}
}
private void connectTCPServer() {
Socket socket = null;
while (socket == null) {
try {
socket = new Socket("localhost", 8688);
mClientSocket = socket;
mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED);
Log.i(TAG, "connect server success.");
} catch (IOException e) {
e.printStackTrace();
SystemClock.sleep(1000);
Log.i(TAG, "connect tcp server failed, retry...");
}
}
//接收服务端消息
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (!SocketActivity.this.isFinishing()){
String msg = br.readLine();
Log.e(TAG, "receive msg:" + msg);
if(null != msg) {
String time = new SimpleDateFormat("HH:mm:ss").format(new Date(System.currentTimeMillis()));
final String showMsg = "server" + time + ":" + msg + "\n";
mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG, showMsg).sendToTarget();
}
}
close(mPrintWriter);
close(br);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void close(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mClientSocket != null) {
try {
mClientSocket.shutdownInput();
mClientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上述就是通过 Socket 来进行进程间通信的实例,除了采用 TCP 套接字,还可以采用 UDP 套接字,另外,上面的例子仅仅是一个示例,实际上通过 Socket 不仅能实现进程间他通信,还可以实现设备间的通信,当然前提是这些设备之间的 IP 地址互相可见,这里就不一一说明了。下面我们看下运行效果: