基于TCP的安卓客户端开发
一.Socket通信简介
Android与服务器的通信方式主要有两种,一是Http通信,一是Socket通信。两者的最大差异在于,http连接使用的是“请求—响应方式”,即在请求时建立连接通道,当客户端向服务器发送请求后,服务器端才能向客户端返回数据。而Socket通信则是在双方建立起连接后就可以直接进行数据的传输,在连接时可实现信息的主动推送,而不需要每次由客户端向服务器发送请求。 那么,什么是socket?Socket又称套接字,在程序内部提供了与外界通信的端口,即端口通信。通过建立socket连接,可为通信双方的数据传输传提供通道。socket的主要特点有数据丢失率低,使用简单且易于移植。
- 什么是Socket?
是一种抽象层,应用程序通过它来发送和接收数据,使用Socket可以将应用程序添加到网络中,与处于同一网络中的其他应用程序进行通信。简单来说,Socket提供了程序内部与外界通信的端口并为通信双方的提供了数据传输通道。
- Socket的分类
根据不同的的底层协议,Socket的实现是多样化的。本指南中只介绍TCP/IP协议族的内容,在这个协议族当中主要的Socket类型为流套接字(streamsocket)和数据报套接字(datagramsocket)。流套接字将TCP作为其端对端协议,提供了一个可信赖的字节流服务。数据报套接字使用UDP协议,提供数据打包发送服务。 下面,我们来认识一下这两种Socket类型的基本实现模型。
二.Socket基本通信模型

- TCP通信模型

- UDP通信模型

三.Socket基本实现原理
- 基于TCP协议的Socket
服务器端首先声明一个ServerSocket对象并且指定端口号,然后调用Serversocket的accept()方法接收客户端的数据。accept()方法在没有数据进行接收的处于堵塞状态。(Socketsocket=serversocket.accept()),一旦接收到数据,通过inputstream读取接收的数据。
客户端创建一个Socket对象,指定服务器端的ip地址和端口号(Socketsocket=newSocket("172.168.10.108",8080);),通过inputstream读取数据,获取服务器发出的数据(OutputStreamoutputstream=socket.getOutputStream()),最后将要发送的数据写入到outputstream即可进行TCP协议的socket数据传输。
- 基于UDP协议的数据传输
服务器端首先创建一个DatagramSocket对象,并且指点监听的端口。接下来创建一个空的DatagramSocket对象用于接收数据(bytedata[]=newbyte[1024;]DatagramSocketpacket=newDatagramSocket(data,data.length)),使用DatagramSocket的receive方法接收客户端发送的数据,receive()与serversocket的accepet()类似,在没有数据进行接收的处于堵塞状态。
客户端也创建个DatagramSocket对象,并且指点监听的端口。接下来创建一个InetAddress对象,这个对象类似与一个网络的发送地址(InetAddressserveraddress=InetAddress.getByName("172.168.1.120")).定义要发送的一个字符串,创建一个DatagramPacket对象,并制定要讲这个数据报包发送到网络的那个地址以及端口号,最后使用DatagramSocket的对象的send()发送数据。*(Stringstr="hello";bytedata[]=str.getByte();DatagramPacketpacket=new DatagramPacket(data,data.length,serveraddress,4567);socket.send(packet);)
注:以上通信原理内容非笔者总结,引于此目的在于“实践在即,理论先行”
四.权限申明
1 <!--允许应用程序改变网络状态--> 2 <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> 3 4 <!--允许应用程序改变WIFI连接状态--> 5 <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> 6 7 <!--允许应用程序访问有关的网络信息--> 8 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 9 10 <!--允许应用程序访问WIFI网卡的网络信息--> 11 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 12 13 <!--允许应用程序完全使用网络--> 14 <uses-permission android:name="android.permission.INTERNET"/>
五.布局文件
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <LinearLayout 7 android:layout_width="match_parent" 8 android:layout_height="wrap_content" 9 android:layout_marginLeft="10dp" 10 android:layout_marginRight="10dp" 11 > 12 <TextView 13 android:layout_width="0dp" 14 android:layout_height="match_parent" 15 android:layout_weight="1" 16 android:layout_marginTop="12dp" 17 android:layout_gravity="center" 18 android:text="IP:" 19 android:textSize="20dp"/> 20 <EditText 21 android:id="@+id/IPEditText" 22 android:layout_width="0dp" 23 android:layout_height="match_parent" 24 android:gravity="center" 25 android:layout_weight="4" 26 android:text="223.3.200.4" 27 android:textSize="20dp" 28 /> 29 <TextView 30 android:layout_width="0dp" 31 android:layout_height="match_parent" 32 android:layout_weight="2" 33 android:layout_marginTop="12dp" 34 android:layout_gravity="center" 35 android:text="Port:" 36 android:textSize="20dp"/> 37 <EditText 38 android:id="@+id/PortEditText" 39 android:layout_width="0dp" 40 android:layout_height="match_parent" 41 android:gravity="center" 42 android:layout_weight="2" 43 android:text="8086" 44 android:textSize="20dp" 45 /> 46 <Button 47 android:id="@+id/ConnectButton" 48 android:layout_width="0dp" 49 android:layout_height="match_parent" 50 android:layout_weight="1" 51 android:text="连接"/> 52 53 </LinearLayout> 54 <LinearLayout 55 android:layout_width="match_parent" 56 android:layout_height="wrap_content" 57 android:layout_marginLeft="10dp" 58 android:layout_marginRight="10dp" 59 60 > 61 <EditText 62 android:id="@+id/MessagetoSendEditText" 63 android:layout_width="0dp" 64 android:layout_height="match_parent" 65 android:layout_weight="3" 66 android:text="Data from Client"/> 67 <Button 68 android:id="@+id/SendButton" 69 android:layout_width="0dp" 70 android:layout_height="wrap_content" 71 android:layout_weight="1" 72 android:text="发送"/> 73 </LinearLayout> 74 <LinearLayout 75 android:layout_width="match_parent" 76 android:layout_height="wrap_content" 77 android:layout_marginLeft="10dp" 78 android:layout_marginRight="10dp" 79 > 80 <TextView 81 android:id="@+id/DisplayTextView" 82 android:layout_width="match_parent" 83 android:layout_height="wrap_content" 84 android:layout_marginTop="10dp" 85 android:hint="Display area:" 86 android:textSize="20dp"/> 87 </LinearLayout> 88 89 90 </LinearLayout>
六.具体代码实现
1 package com.example.john.androidsockettest_client; 2 3 import android.os.Handler; 4 import android.os.Message; 5 import android.support.v7.app.AppCompatActivity; 6 import android.os.Bundle; 7 import android.view.View; 8 import android.widget.Button; 9 import android.widget.EditText; 10 import android.widget.TextView; 11 import android.widget.Toast; 12 13 import java.io.BufferedReader; 14 import java.io.IOException; 15 import java.io.InputStreamReader; 16 import java.io.OutputStream; 17 import java.net.InetSocketAddress; 18 import java.net.Socket; 19 import java.net.SocketTimeoutException; 20 21 public class MainActivity extends AppCompatActivity { 22 23 //Handler中的消息类型 24 public static final int DEBUG = 0x00; 25 public static final int RECEIVEDATAFROMSERVER = 0x01; 26 public static final int SENDDATATOSERVER = 0x02; 27 //线程 28 Socket socket = null; //成功建立一次连接后获得的套接字 29 ConnectThread connectThread; //当run方法执行完后,线程就会退出,故不需要主动关闭 30 SendThread sendThread; //发送线程,由send按键触发 31 ReceiveThread receiveThread; //接收线程,连接成功后一直运行 32 //待发送的消息 33 public String messagetoSend = ""; 34 //控件 35 TextView displayTextView; //显示接收、发送的数据及Debug信息 36 Button sendButton; //发送按钮,点击触发发送线程 37 Button connectButton; //连接按钮,点击触发连接线程 38 EditText messagetoSendEditText; 39 EditText iPEditText; 40 EditText portEditText; 41 42 public Handler myHandler = new Handler() { 43 @Override 44 public void handleMessage(Message msg) { 45 if (msg.what == RECEIVEDATAFROMSERVER) { 46 Bundle bundle = msg.getData(); 47 displayTextView.append("Server:"+bundle.getString("string1")+"\n"); 48 } 49 else if (msg.what == DEBUG) { 50 Bundle bundle = msg.getData(); 51 displayTextView.append("Debug:"+bundle.getString("string1")+"\n"); 52 } 53 else if (msg.what == SENDDATATOSERVER) { 54 Bundle bundle = msg.getData(); 55 displayTextView.append("Client:"+bundle.getString("string1")+"\n"); 56 } 57 } 58 59 }; 60 //子线程更新UI 61 public void SendMessagetoHandler(final int messageType , String string1toHandler){ 62 Message msg = new Message(); 63 msg.what = messageType; //消息类型 64 Bundle bundle = new Bundle(); 65 bundle.clear(); 66 bundle.putString("string1", string1toHandler); //向bundle中添加字符串 67 msg.setData(bundle); 68 myHandler.sendMessage(msg); 69 } 70 71 @Override 72 protected void onCreate(Bundle savedInstanceState) { 73 super.onCreate(savedInstanceState); 74 setContentView(R.layout.activity_main); 75 displayTextView = (TextView) findViewById(R.id.DisplayTextView); 76 sendButton = (Button) findViewById(R.id.SendButton); 77 messagetoSendEditText = (EditText) findViewById(R.id.MessagetoSendEditText); 78 iPEditText = (EditText)findViewById(R.id.IPEditText); 79 portEditText = (EditText)findViewById(R.id.PortEditText); 80 connectButton = (Button)findViewById(R.id.ConnectButton); 81 82 connectButton.setOnClickListener(new View.OnClickListener() { 83 @Override 84 public void onClick(View v) { 85 connectThread= new ConnectThread(); 86 connectThread.start(); 87 } 88 }); 89 sendButton.setOnClickListener(new View.OnClickListener() { 90 91 @Override 92 public void onClick(View v) { 93 messagetoSend = messagetoSendEditText.getText().toString(); 94 //使用连接成功后得到的socket构造发送线程,每点击一次send按钮触发一次发送线程 95 sendThread = new SendThread(socket); 96 sendThread.start(); 97 } 98 }); 99 } 100 101 //******** 连接线程 ********** 102 class ConnectThread extends Thread{ 103 @Override 104 public void run() { 105 try{ 106 //连接服务器 并设置连接超时为1秒 107 socket = new Socket(); 108 socket.connect(new InetSocketAddress(iPEditText.getText().toString(), 109 Integer.parseInt(portEditText.getText().toString())), 1000); 110 }catch (SocketTimeoutException aa) { 111 //更新UI:连接失败 112 SendMessagetoHandler(DEBUG,"服务器连接失败!"); 113 return; //直接返回 114 } catch (IOException e) { 115 e.printStackTrace(); 116 117 } 118 //更新UI:连接成功 119 SendMessagetoHandler(DEBUG,"服务器连接成功!"); 120 121 //打开接收线程 122 receiveThread = new ReceiveThread(socket); 123 receiveThread.start(); 124 } 125 } 126 //******** 发送线程 ********** 127 class SendThread extends Thread{ 128 private Socket mSocket; 129 //发送线程的构造函数,由连接线程传入套接字 130 public SendThread(Socket socket) {mSocket = socket;} 131 132 @Override 133 public void run() { 134 try{ 135 OutputStream outputStream = mSocket.getOutputStream(); 136 //向服务器发送信息 137 outputStream.write(messagetoSend.getBytes("gbk")); 138 outputStream.flush(); 139 //更新UI:显示发送出的数据 140 SendMessagetoHandler(SENDDATATOSERVER,messagetoSend); 141 }catch (IOException e) { 142 e.printStackTrace(); 143 //更新UI:显示发送错误信息 144 SendMessagetoHandler(DEBUG,"发送失败!"); 145 return; 146 } 147 } 148 } 149 150 //******** 接收线程 ********** 151 class ReceiveThread extends Thread{ 152 153 private Socket mSocket; 154 //接收线程的构造函数,由连接线程传入套接字 155 public ReceiveThread(Socket socket){mSocket = socket;} 156 157 @Override 158 public void run() { 159 while(true){ //连接成功后将一直运行 160 try { 161 BufferedReader bufferedReader; 162 String line = null; 163 String readBuffer=""; 164 bufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); 165 while ((line = bufferedReader.readLine()) != null) { 166 readBuffer = line + readBuffer; 167 SendMessagetoHandler(RECEIVEDATAFROMSERVER,readBuffer); 168 readBuffer = ""; 169 } 170 bufferedReader.close(); 171 }catch (IOException e) { 172 e.printStackTrace(); 173 //更新UI:显示发送错误信息 174 SendMessagetoHandler(DEBUG,"接收失败!"); 175 return; 176 } 177 } 178 } 179 } 180 }
采用多线程编程,输入IP地址和端口号后点击“连接”按钮开启ConnectThread,在该线程中执行对服务器的连接,由连接成功后的socket构造ReceiveThread并启动,在该线程中执行while(true){}循环持续监听输入流,并通过Handler异步消息处理机制更新主UI(Server:)。点击“发送”按钮,由socket构造SendThread并启动,向服务器端发送数据并更新主UI(Client:)。在子线程运行过程中,出现错误会向主UI呈递调试信息(Debug:)。以上即是该TCP客户端主要的编程思想。
七.Debug
- 关于readLine()
在写ReceiveThread线程代码块时,笔者最初读取输入流的写法如下:
1 while ((line = bufferedReader.readLine()) != null) { 2 readBuffer = line + readBuffer; 3 readBuffer = ""; 4 } 5 SendMessagetoHandler(RECEIVEDATAFROMSERVER,readBuffer);
结果在主UI一直没有看到消息更新(直到把服务器端口关闭,服务器之前发的数据才更新到UI),这就说明第5行代码根本没有实现,循环没有出来。后来将第5行代码放入while循环内部运行正常。
原因如下:
误以为readLine()是读取到没有数据时就返回null(因为其它read方法当读到没有数据时返回-1),而实际上readLine()是一个阻塞函数,当没有数据读取时,就一直会阻塞在那,而不是返回null;因为readLine()阻塞后,第5行代码根本就不会执行到,所以在UI不会显示接收到的数据。要想执行到第5行,一个办法是发送完数据后就关掉流,这样readLine()结束阻塞状态,而能够得到正确的结果,但显然不能传一行就关一次数据流;另外一个办法是把第5行代码放到while循环体内。
readLine()只有在数据流发生异常或者另一端被close()掉时,才会返回null值。
如果不指定buffer大小,则readLine()使用的buffer有8192个字符。在达到buffer大小之前,只有遇到"/r"、"/n"、"/r/n"才会返回。
- 使用InputStream类中的read(b:byte[]):int函数
1 //另一种读取方式 :+read(b:byte[]):int 从输入流中读取b.length个字节到 2 // 数组b中,并且返回实际读取的字节数。到流的最后返回-1 3 byte[] b = new byte[1024]; //每个数据包最大1024个字节 4 int length = 0; 5 while((length = mSocket.getInputStream().read(b)) != -1){ //在流产生时执行里面的代码 6 String r_msg = new String(b,0,length,ISO_ENCODE); 7 SendMessagetoHandler(RECEIVEDATAFROMSERVER,r_msg); 8 //SendMessagetoHandler(DEBUG,"length="+length); 9 }
这种方式不要求发送端在数据包后添加回车换行标志,所写即所发。
八.实际运行效果

如果这篇blog对您有所帮助,请留下您的一个赞,也欢迎留言讨论,笔者将在第一时间回复!

浙公网安备 33010602011771号