浅谈TCP协议在android中的使用

本文详细介绍Android平台上使用TCP Socket实现客户端和服务端通信的过程。包括ServerSocket和Socket的基本使用方法,多线程下的通信处理,以及如何确保跨平台字符编码的一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言
手机作为移动设备,很受大家的青睐,因为它携带方便,可以随时随地上网聊天、玩游戏(如现在最火王者某某),这些都是在联网的情况进行的,如果手机不能上网的话,那么它就是没有用的铁疙瘩(某某奇葩也就不会为了某某果机割肾伤身了),因此网络对手机来说有着至关重要的作用。
由于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为本机IP
			ServerSocket 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-permission android :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 block
				e.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 block
				e.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 block
					System.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 block
			e.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)后,客户端才会收到服务端传送过来的数据呢?谁有解决之法、或知其原理务必指点一下小编。
参考文献:
《疯狂android讲义》









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值