浅谈TCP协议在android中的使用

来源:互联网 发布:html注册页面源码 编辑:程序博客网 时间:2024/05/17 22:50
前言
手机作为移动设备,很受大家的青睐,因为它携带方便,可以随时随地上网聊天、玩游戏(如现在最火王者某某),这些都是在联网的情况进行的,如果手机不能上网的话,那么它就是没有用的铁疙瘩(某某奇葩也就不会为了某某果机割肾伤身了),因此网络对手机来说有着至关重要的作用。
由于JDK本身集成了TCP、UDP网路协议,那么Android 完全支持它;Android 也可以使用ServerSocket(服务端)、Socket(客户端),而ServerSocket、Socket是基于TCP/IP协议的网络通信;Android也可以使用DatagramSocket、DatagramPacket、MulticastSocket来建立基于UDP协议的网络通信;同时android也支持JDK提供URL、URLConnection;当然android还内置了HttpClient(这个自从android5.0以后需要自己添加Jar包),这样可以非常方便地发送HTTP请求,并获取HTTP响应;但是android并没有内置Web Service的支持,为了弥补这方面的不足,需要使用第三方类库(KSOAP2)来调用WebService。
一 TCP协议
TCP/IP是一种可靠的网络通信协议,在通信的两端各需要建立一个Socket,从而在两端之间形成虚拟链路,两端的程序可以通过虚拟链路进行通信。如下:

IP协议负责将消息从一个主机传送到另一个主机,信息在传送的过程中被分割成几个小包。
TCP协议负责提供可靠并且无差错的通信服务,它也被称为一种端对端的协议,这是因为,它为两台计算机之间的连接起到重要的作用,当一台计算机需要与另一台计算机连接时,TCP协议会让它们建立一个连接:用于发送和接收数据的虚拟链路
TCP负责收集这些信息包,并将其按适当的次序排好传送,在接收端再将其正确地还原。TCP协议保证了数据包在传送中准确无误。同时TCP使用重发机制:当一个通信实体发送一个信息给另一个通信实体后,需要收到另一个通信实体的确认信息,如果没有收到另一个通信实体的确认信息,则会再次重发刚才发送的信息。
综上所述,虽然IP和TCP这两个协议的功能不尽相同,也可以分开单独使用,但它们时在同一个时期作为一个协议来设计的,并且在功能上也是互补的。只有两者结合才能保证Internet在复杂的环境下正常的运行。凡是连接到Internet计算机,都必须安装和使用这两个协议。
二 ServerSocket创建TCP服务端
两个通信实体在进行通信,必须有服务端、客户端之分,而Java中建立服务端是使用ServerSocket,下面看一些它的方法:
ServerSocket(int port):构造函数,参数port是端口,有效值:0~65535。
ServerSocket(int port,int backlog):构造函数,增加一个用来改变连接队列长度的参数backlog。
ServerSocket(int port,int backlog,InetAddress localAddr):构造函数,参数localAddr指定本地IP地址,用于在本地存在过个IP地址的情况。
Socket accept():如果接受到一个客户端Socket的连接请求,该方法将会返回与连接客户端Socket对应的Socket;否则该方法将会一直处于等待状态,线程也被阻塞。
接下来看一个示例:
 public static void main(String[] args) {      System.out.print("开启服务!");try {//创建一个ServerSocket 对象实例,用来监听客户端Socket的连接状态,端口号为30000,IP为本机IPServerSocket ss = new ServerSocket(30000);//无限循环,不断接收来自客户端Socket的请求,没有Socket请求时会进入阻塞状态。while(true){Socket  socket = ss.accept();//获取输出流,向Socket通道中写入数据OutputStream os = socket.getOutputStream();os.write("您好,恭喜您中奖了!".getBytes("utf-8"));//关闭通道os.close();socket.close();}} catch (IOException e) {e.printStackTrace();} }
完整代码点击查看。
注意:
上面的输出流OutputStream并没有装成PrintStream,然后直接输出,整个字符串,这是由于服务端程序运行于window主机上,当直接使用PrintStream输出整个字符串默认使用系统平台的字符编码GBK,而android是在Linux平台运行的,客户端在读取网络数据时,默认使用的UTF-8编码,这样势必引起乱码。所以为保证能够正确解析数据,要手动控制字符串的编码,强行指定使用UTF-8字符集进行编码。
三 Socket创建TCP客户端
先来看下socket的构造函数:
Socket(InetAddress/String remoteAddress,int port):参数remoteAddress指定远程主机的IP地址,port指定远程主机的端口,这里没有指定本地IP地址,本地端口,默认使用本机的IP地址,默认使用系统动态分配的端口。
Socket(InetAddress/String remoteAddress,int port,InetAddress localAddr, int localPort):增加参数localAddr指定本地Ip地址,localPort指定本地端口,这种情况使用本地主机有多个IP地址的情形。
再看其他重要的方法:
InputStream getInputStream():通过Socket对象实例获取输入流,让程序可以从Socket通道中获取数据。
OutputStream getOutputStream():通过Socket对象实例获取输出流,让程序可以往Socket通道中写入数据。
示例:
 new Thread(new Runnable() {            @Override            public void run() {                try {                    //建立连接到远程服务器的socket                    Socket socket = new Socket("192.168.11.139",30000);                    //=================或者==============                    //创建Socket对象                    Socket s = new Socket();                    //连接远程服务                    s.connect(new InetSocketAddress("192.168.11.139",30000));                    //设置超时时间                    socket.setSoTimeout(10000);                    BufferedReader bufferedReader  = new BufferedReader(new InputStreamReader(socket.getInputStream()));                    //进行I/O 操作                    final String result = bufferedReader.readLine();                    if (!TextUtils.isEmpty(result)){                        ServerSocketActivity.this.runOnUiThread(new Runnable() {                            @Override                            public void run() {                                txt_serverSocket.setText(result);                            }                        });                    }                    //关闭输入流、socket                    bufferedReader.close();                    socket.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }).start();
完整代码点击查看。
说明,到这里服务端、客户端通信的虚拟链路算是建立起来了,要实现通信客户端还要添加权限:
<uses-permissionandroid:name="android.permission.INTERNET"/>
接下来先运行服务器,再运行客户端,那么客户端会收到字符串"您好,恭喜您中奖了!"
四 加入多线程
在上面示例中已经简单的叙述了Socket的通信的过程,上面的示例中是一对一的关系,就是一个服务器对应一个客户端,那么在实际使用中却不是这样的,一个服务器需要服务多个对象(客户端),且可能需要与每个客户端保持长时间的通信,即服务端不断读取客户端的数据,并向客户端写入数据,而客户端也是如此。
当使用readLine()方法读取数据,该方法成功返回之前,线程被阻塞,程序是无法进行下去,也就不能接受其他的Socket连接请求了,所以为了解决这个问题,服务端为每个Socket启动一个新的线程,该线程负责与客户端进行通信,这样就不影响服务器接受其他线程了。
服务端:
首先定义一个类继承Runnable(例如我的类名叫作ServerThread),通过构造函数把Socket对象实例传递来,这样就可以获取输入流、输出流了,如:
public ServerThread(Socket s) {this.s = s;if (null != s) {try {br = new BufferedReader(new InputStreamReader(s.getInputStream(),"utf-8"));} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
再者读取Socket中的数据如下:
/** * 读取客户端数据 * @return */public String readFromClient () {if(null != br) {try {String result = br.readLine();System.out.println("服务端读取:" +result);return result;} catch (IOException e) {System.out.println("读取失败!");//捕捉到异常,表明s 对应的客户端已经关闭MutiThreadServerSocket.socketList.remove(s);// TODO Auto-generated catch blocke.printStackTrace();}}return null;}
还可以把刚刚获取的数据转发给所有的客户端
String content = null;// TODO Auto-generated method stub        while((content = readFromClient()) != null) {           //MutiThreadServerSocket.socketList为ArrayList<Socket>类型用来记录每个客户端           for(Socket s : MutiThreadServerSocket.socketList) {           try {OutputStream os = s.getOutputStream();os.write(content.getBytes("utf-8"));os.flush();//os.close();System.out.println("服务端写入content:" + content);} catch (IOException e) {// TODO Auto-generated catch blockSystem.out.println("服务端写入失败!");e.printStackTrace();}           }        }
完整代码
最后就是服务程序:
//存储Socket对象     public static ArrayList<Socket> socketList = new ArrayList<>();public static void main(String[] args) {System.out.println("开启多线程服务!");try {ServerSocket ss = new ServerSocket(30010);while(true){Socket  socket = ss.accept();    socketList.add(socket);    //每当一个客户端连接之后,就启动一个线程    new Thread(new ServerThread(socket)).start();}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}    }
完整代码
客户端:
首先定义一个类继承Runnable,如下:
public class ClientThread implements Runnable {    private Handler handler;    private Socket socket;    //输入流 读取服务端传送过来的消息    private BufferedReader br;    //输出流往服务器发送信息    private static OutputStream outputStream;    public static Handler  revHandler ;    private InputStream inputStream;    //计时器    private Timer timer  = new Timer();    public ClientThread(Handler handler) {        this.handler = handler;    }    @Override    public void run() {        try {            //192.168.11.139 为本地电脑的ip            socket = new Socket("192.168.11.139",30010);//            socket.setKeepAlive(true);            inputStream = socket.getInputStream();            br = new BufferedReader(new InputStreamReader(inputStream));            outputStream = socket.getOutputStream();            Log.e("进入循环线程:","-------------->");            /**             *             * 开子线程读取数据,因为readLine()会导致阻塞             * 对于socket,不能认为把某次写入到流中的数据读取完了就算流结尾了,             * 但是socket流还存在,还可以继续往里面写入数据然后再读取。             * 这时用BufferedReader封装socket的输入流,调用BufferedReader的readLine方法是不会返回null的             * 所以在循环内如果不判断   content!=null && content.length() > 0  那么程序将会一直阻塞在这里(程序是因为readLine阻塞,并不是死循环)             *             */            new Thread(new Runnable() {                @Override                public void run() {                    Log.e("开始读取:","1111");                    String content  = null;                    try {                        while (((content = br.readLine()) != null) && (content.length() > 0)){                            Log.e("读取====》",content);                            //每当有消息时,及时通知主界面更新消息                            Message  message  = new Message();                            message.what = 0x123;                            message.obj  = content;                            handler.sendMessage(message);                        }                    } catch (IOException e) {                        closeSocket();                        e.printStackTrace();                    }                }            }).start();            Looper.prepare();            revHandler = new Handler(){                @Override                public void handleMessage(Message msg) {                    super.handleMessage(msg);                    if (msg.what == 0x345){                        try {                            outputStream.write((msg.obj.toString() + "\r\n").getBytes("utf-8"));                            Log.e("写入====》",msg.obj.toString());                            outputStream.flush();                        } catch (UnsupportedEncodingException e) {                            closeSocket();                            e.printStackTrace();                        } catch (IOException e) {                            closeSocket();                            e.printStackTrace();                        }                    }                }            };            //把当前的线程初始化为looper//,循环读取服务端发过来的消息            Looper.loop();        } catch (IOException e) {            closeSocket();            e.printStackTrace();        }    }    /**     * 关闭端口     */    private void  closeSocket(){            try {                if (null != inputStream){                   inputStream.close();                }                if (null != outputStream){                    outputStream.close();                }                if (null != br){                    br.close();                }                if (null != socket){                    socket.close();                }            } catch (IOException e) {                e.printStackTrace();            }    }}
完整代码
然后在主界面添加一个输入框和发送按钮,输入内容后,点击发送把内容发送给服务端,布局文件如下:
<?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:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context="com.ecric.http.socket.MultiThreadClientActivity">    <LinearLayout        android:orientation="horizontal"        android:layout_width="match_parent"        android:layout_height="wrap_content">        <EditText            android:id="@+id/ed_multi_input"            android:layout_width="0dp"            android:layout_weight="3"            android:layout_height="wrap_content" />        <Button            android:id="@+id/btn_send"            android:text="发送"            android:layout_width="0dp"            android:layout_weight="1"            android:layout_height="wrap_content" />    </LinearLayout>    <!--用来展示文本信息-->    <TextView        android:id="@+id/txt_show"        android:text="展示文本信息:"        android:layout_width="match_parent"        android:layout_height="wrap_content" /></LinearLayout>
最后就是客户端程序:
@EActivity(R.layout.activity_multi_thread_client)public class MultiThreadClientActivity extends AppCompatActivity {    // 输入信息框    @ViewById(R.id.ed_multi_input)    EditText edInput;    //展示信息    @ViewById(R.id.txt_show)    TextView txtShow;    public Handler handler = new Handler(){        @Override        public void handleMessage(Message msg) {            super.handleMessage(msg);            //如果消息来自于子线程            if (msg.what == 0x123){                txtShow.append("\n" + msg.obj.toString());            }        }    };    //客户端处理数据的线程    private ClientThread clientThread;    @AfterViews    public void initData(){        clientThread = new ClientThread(handler);        new Thread(clientThread).start();//        ClientBody clientBody = new ClientBody("192.168.11.139",30010,"1");//        clientBody.start();    }    /**     * 发送监听     * @param view     */    @Click(R.id.btn_send)    public void onClick(View view){        //当用户按下发送按钮后,将用户输入的数据,封装成Message        //然后发送给子线程的handle对象        Message message  = new Message();        message.what = 0x345;        message.obj = edInput.getText().toString();        clientThread.revHandler.sendMessage(message);        edInput.setText("");    }}
说明在这个程序也没有处理多少,主要是启动ClientThread,及与输入操作,当用户点击“发送”按钮之后,程序将会吧输入的内容发送给ClientThread的revHandle,进而把内容写到Socket中,传送为服务端;同样从服务读取到内容时,将会把读取到的信息通过handle发送给UI线程,进而更新内容;还有一点要说说明下,上述代码我使用AndroidAnnotations注解框架,使代码看起来简单很多,不感兴趣的可以不管它,查看控件时,老老实实findViewById就可以了。
接下来先运行服务端,再运行客户端,在输入框输入QWERTY点击发送,服务端打印结果:
开启多线程服务!
服务端读取:QWERTY
服务端写入content:QWERTY
客户端结果:

但在此过程中遇到一个问题:服务端可以很好的接受来自客户端数据,但为什么只有服务端进程关闭(或者说在服务端关闭socket)后,客户端才会收到服务端传送过来的数据呢?谁有解决之法、或知其原理务必指点一下小编。
服务端项目代码:https://git.oschina.net/lzbgit/SocketServerTest
客服端项目代码:https://git.oschina.net/lzbgit/HttpProtocolApp
参考文献:
《疯狂android讲义》
http://www.cnblogs.com/sweetchildomine/p/6477563.html