两台计算机进行通信,在网络中我们可以通过 IP 地址定位到具体的某一台计算机,然后再通过端口号定位到这台计算机的某一个应用程序。这样我们的一台电脑就可以通过 IP 地址和端口号和另一台电脑进行相应的通讯了。那我们定位到了具体的某台计算机怎么和它进行相应的通信呢?他们之间的通信方式就是使用 Socket 提供的编程接口。Socket 称为 "套接字",通俗的理解就是它提供了进程间通信的方式,然后提供了供进程间通信的相应的编程接口。
总体来讲,Socket 通信分为流式套接字和用户数据报套接字两种,分别对应于网络传输控制层的 TCP 和 UDP 协议。
TCP 协议是面向连接的协议,提供稳定的双向通信功能,TCP 连接的建立需要经过 "三次握手" 才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而 UDP 是无连接的,提供不稳定的单项通信功能,当然 UDP 也可以实现双向通信功能。在性能上,UDP 具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。
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 类
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: 客户端连接中断!效果图: