什么是Socket?
两台计算机进行通信,在网络中我们可以通过 IP 地址定位到具体的某一台计算机,然后再通过端口号定位到这台计算机的某一个应用程序。这样我们的一台电脑就可以通过 IP 地址和端口号和另一台电脑进行相应的通讯了。那我们定位到了具体的某台计算机怎么和它进行相应的通信呢?他们之间的通信方式就是使用 Socket 提供的编程接口。Socket 称为 "套接字",通俗的理解就是它提供了进程间通信的方式,然后提供了供进程间通信的相应的编程接口。
总体来讲,Socket 通信分为流式套接字和用户数据报套接字两种,分别对应于网络传输控制层的 TCP 和 UDP 协议。
TCP 协议是面向连接的协议,提供稳定的双向通信功能,TCP 连接的建立需要经过 "三次握手" 才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而 UDP 是无连接的,提供不稳定的单项通信功能,当然 UDP 也可以实现双向通信功能。在性能上,UDP 具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。
简单说一下 UDP 和 TCP:
1. UDP
UDP 通信模型图:
图解: UDP Client (客户端)通过 ip 地址和端口 port 定位到相应的 UDP Server(服务端),然后把信息通过 DatagramPacket 类打包后再通过由 DatagramSocket 类中的 send 方法发送给服务端,服务端接收到之后通过 DatagramSocket 类解析然后再以同样的传递方式回复给客户端。
UDP 通信的方式很像写信的方式,发送方把信息写到一封信中,然后将信再传递给收信方,然后收信方收到信后打开信封读信,然后再以同样的方式决定是否回信给发信方。由于我们不能确定信发出之后对方是否能收到,所以通常把 UDP 定义为不安全的一种通信协议。
涉及到的 API :
- InetAddress (ip、port 的封装类)
- DatagramSocket( receive, send)
- DatagramPacket
2. TCP
TCP 通信模型图:
TCP类似于打电话的方式。一旦接通之后我们可以进行不间断的通信,实际上它是一种比较安全的协议。
涉及到的 API:
- ServerSocket (Server端)
- Socket (Client端)
- 以及相关 IO 类
实战: 使用 Socket 通信中的 TCP 方式完成进程间通信
1. 首先因为要使用网络通信,所以要在 AndroidManifest.xml 文件中声明权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
2. 服务端代码(使用 ServerSocket),TCPServerService.java:
package com.cfm.sockettest;
public class TCPServerService extends Service {
private static final String TAG = "cfmtest";
// 当服务销毁时,结束服务端与客户端之间通信的线程
private boolean mIsServiceDestoryed = false;
private String[] mDefinedMessages = new String[]{
"坚决的信心,能使平凡的人们,做出惊人的事业。——马尔顿",
"人生是个圆,有的人走了一辈子也没有走出命运画出的圆圈,其实,圆上的每一个点都有一条腾飞的切线。",
"要有自信,然后全力以赴——假如具有这种观念,任何事情十之八九都能成功。——威尔逊",
"行动是治愈恐惧的良药,而犹豫、拖延将不断滋养恐惧。",
"天道酬勤。也许你付出了不一定得到回报,但不付出一定得不到回报。",
"一个人是否有成就只有看他是否具有自尊心和自信心两个条件。——苏格拉底",
"宝剑锋从磨砺出,梅花香自苦寒来。"
};
@Override
public void onCreate() {
super.onCreate();
new Thread(new TcpServer()).start();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class TcpServer implements Runnable{
@Override
public void run() {
ServerSocket serverSocket = null;
try {
// 创建服务端 Socket
serverSocket = new ServerSocket(8868);
} catch (IOException e) {
e.printStackTrace();
}
// 接收客户端的请求
while (!mIsServiceDestoryed){
try {
if (serverSocket != null) {
// 连接到服务端的客户端 Socket 对象
final Socket socket = serverSocket.accept();
Log.d(TAG, "服务端接收到客户端的连接请求!");
new Thread(new Runnable() {
@Override
public void run() {
try {
responseClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void responseClient(Socket clientSocket) throws IOException{
// 用于接收客户端消息
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 用于向客户端发送消息
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())), true);
while (!mIsServiceDestoryed){
String str = in.readLine();
Log.d(TAG, "服务端接收到客户端发送的消息: " + str);
if(str == null)break;
// 随机回复一句箴言
int i = new Random().nextInt(mDefinedMessages.length);
String msg = mDefinedMessages[i];
out.println(msg); // 服务端向客户端回复消息
Log.d(TAG, "服务端发送给客户端的箴言: " + msg);
}
Log.d(TAG, "服务端连接断开!");
in.close();
out.close();
clientSocket.close();
}
@Override
public void onDestroy() {
super.onDestroy();
mIsServiceDestoryed = true;
}
}
3. 客户端代码,TCPClientActivity.java:
package com.cfm.sockettest;
public class TCPClientActivity extends AppCompatActivity {
private static final String TAG = "cfmtest";
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 PrintWriter mPrintWriter;
// 客户端 Socket
private Socket mClientSocket;
// 通过 Handler 从子线程切换到主线程以方便更新 UI
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_RECEIVE_NEW_MSG:
mMessageTextView.setText(mMessageTextView.getText() + (String) msg.obj);
break;
case MESSAGE_SOCKET_CONNECTED:
// 与服务端 Socket 连接成功,使能发送按钮
mSendButton.setEnabled(true);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tcpclient);
mMessageEditText = findViewById(R.id.msg);
mMessageTextView = findViewById(R.id.msg_container);
mSendButton = findViewById(R.id.send);
// 开始远程 Service
Intent intent = new Intent(this, TCPServerService.class);
startService(intent);
// 连接服务端 Socket
new Thread(new Runnable() {
@Override
public void run() {
connectTCPServer();
}
}).start();
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final String msg = mMessageEditText.getText().toString();
Log.d(TAG,"客户端向服务端发送的消息: " + msg);
if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
new Thread(new Runnable() {
@Override
public void run() {
// 发送消息给客户端,注意这里要在线程中发送,否则会 crash
mPrintWriter.println(msg);
}
}).start();
mMessageEditText.setText(""); // 清空输入框
String time = formatDateTime(System.currentTimeMillis());
String showedMsg = "self " + time + ":" + msg + "\n";
// 将客户端的发送消息的时间和内容显示出来
mMessageTextView.setText(mMessageTextView.getText() + showedMsg);
}
}
});
}
private String formatDateTime(long time) {
return new SimpleDateFormat("(HH:mm:ss)", Locale.CHINA).format(new Date(time));
}
private void connectTCPServer(){
Socket socket = null;
while (socket == null){
// 如果没连接成功就一直尝试连接
try {
socket = new Socket("localhost", 8868);
mClientSocket = socket;
mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
// 连接成功
mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED);
Log.d(TAG, "客户端连接服务端成功!");
} catch (IOException e) {
SystemClock.sleep(1000);
e.printStackTrace();
}
}
// 接收服务端消息
try {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (!TCPClientActivity.this.isFinishing()){
String msg = in.readLine();
Log.d(TAG, "客户端接收到的服务端消息: " + msg);
if(msg != null){
String time = formatDateTime(System.currentTimeMillis());
String showedMsg = "server " + time + ":" + msg + "\n";
mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG, showedMsg).sendToTarget();
}
}
Log.d(TAG, "客户端连接中断!");
in.close();
mPrintWriter.close();
socket.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();
}
}
finish();
}
}
activity_tcpclient.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:background="#ffffff"
tools:context=".TCPClientActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/msg_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/msg"
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_weight="1"
android:ems="10"
android:padding="8dp"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:enabled="false"
android:text="发送"/>
</LinearLayout>
</LinearLayout>
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cfm.sockettest">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".TCPServerService"
android:enabled="true"
android:exported="true"
android:process=":remote">
</service>
<activity android:name=".TCPClientActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Log打印信息:
// 服务端
2019-05-25 19:29:40.546 31371-31395/com.cfm.sockettest:remote D/cfmtest: 服务端接收到客户端的连接请求!
2019-05-25 19:29:44.447 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端接收到客户端发送的消息: 123
2019-05-25 19:29:44.449 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端发送给客户端的箴言: 天道酬勤。也许你付出了不一定得到回报,但不付出一定得不到回报。
2019-05-25 19:29:47.760 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端接收到客户端发送的消息: 456
2019-05-25 19:29:47.761 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端发送给客户端的箴言: 坚决的信心,能使平凡的人们,做出惊人的事业。——马尔顿
2019-05-25 19:29:50.301 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端接收到客户端发送的消息: 789
2019-05-25 19:29:50.302 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端发送给客户端的箴言: 一个人是否有成就只有看他是否具有自尊心和自信心两个条件。——苏格拉底
2019-05-25 19:29:52.963 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端接收到客户端发送的消息: 147
2019-05-25 19:29:52.964 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端发送给客户端的箴言: 行动是治愈恐惧的良药,而犹豫、拖延将不断滋养恐惧。
2019-05-25 19:31:17.807 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端接收到客户端发送的消息: null
2019-05-25 19:31:17.807 31371-31417/com.cfm.sockettest:remote D/cfmtest: 服务端连接断开!
// 客户端
2019-05-25 19:29:40.546 31335-31376/com.cfm.sockettest D/cfmtest: 客户端连接服务端成功!
2019-05-25 19:29:44.442 31335-31335/com.cfm.sockettest D/cfmtest: 客户端向服务端发送的消息: 123
2019-05-25 19:29:44.450 31335-31376/com.cfm.sockettest D/cfmtest: 客户端接收到的服务端消息: 天道酬勤。也许你付出了不一定得到回报,但不付出一定得不到回报。
2019-05-25 19:29:47.755 31335-31335/com.cfm.sockettest D/cfmtest: 客户端向服务端发送的消息: 456
2019-05-25 19:29:47.762 31335-31376/com.cfm.sockettest D/cfmtest: 客户端接收到的服务端消息: 坚决的信心,能使平凡的人们,做出惊人的事业。——马尔顿
2019-05-25 19:29:50.293 31335-31335/com.cfm.sockettest D/cfmtest: 客户端向服务端发送的消息: 789
2019-05-25 19:29:50.302 31335-31376/com.cfm.sockettest D/cfmtest: 客户端接收到的服务端消息: 一个人是否有成就只有看他是否具有自尊心和自信心两个条件。——苏格拉底
2019-05-25 19:29:52.960 31335-31335/com.cfm.sockettest D/cfmtest: 客户端向服务端发送的消息: 147
2019-05-25 19:29:52.965 31335-31376/com.cfm.sockettest D/cfmtest: 客户端接收到的服务端消息: 行动是治愈恐惧的良药,而犹豫、拖延将不断滋养恐惧。
2019-05-25 19:31:54.944 31335-31996/com.cfm.sockettest D/cfmtest: 客户端接收到的服务端消息: null
2019-05-25 19:31:54.944 31335-31996/com.cfm.sockettest D/cfmtest: 客户端连接中断!
效果图: