Android TCP通信的简单实例以及常见问题[超时/主线程阻塞]

时间:2022-01-14 07:00:50

最近在学一些东西。遇到了不少问题。写个笔记吧,我想。

个人更喜欢着眼于实例,从最简单的开始,一步步进行测试。

理论什么的先放一边,把程序跑起来再说。只有跑起来了,才会有动力去继续往下学,参透整个代码的运行机制。


本次的实例目标是——

模拟一个PC服务器与android端的通信,目标是尽量的做到精简,使代码仅留下所需核心部分,降低笔记代码的阅读难度。

--------------------------

>【实例】

PC上的服务器的代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;


public class SocketServer {
//监听端口12345
    private static final int PORT = 12345;  
  
    public static void main(String[] args) {  
        try {  
            System.out.println("等待客户端");  
            ServerSocket serverSocket = new ServerSocket(PORT);  
            Socket clientSocket = serverSocket.accept();  
            System.out.println("客户端上线");
            while (true) {  
                //循环监听客户端请求  
                try {  
                    //获取输入流  
                    BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));  
                    //获取从客户端发来的信息  
                    String msg = in.readLine(); 
                    System.out.println("客户端消息:"+msg);  
                } catch (IOException e) {  
                    System.out.println("读写错误");  
                    e.printStackTrace();  
                } finally {
                serverSocket.close();
                    clientSocket.close();
                    System.out.println("服务器关闭");  
                    break;
                }
            }  
  
        } catch (Exception e) {  
            System.out.println("端口被占用");  
            e.printStackTrace();  
        }  
    }  
}

从中可以看出服务器的搭建主要有以下步骤:

1.建立服务器的Socket,并设定一个监听的端口PORT

ServerSocket serverSocket = new ServerSocket(PORT);

2.将服务器的ServerSocket套接到客户端的Socket上:(未套上时,会一直阻塞。诸位可以试试)

Socket clientSocket = serverSocket.accept();

由于需要进行循环监听,因此获取消息的操作应放在一个while大循环中:

3.从客户端发来的clientSocket上获取输入流的抽象类,然后实例化,并进行读写。

 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));  
String msg = in.readLine(); 

安卓上的客户端代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b=(Button)findViewById(R.id.button);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(net).start();//新的子线程
}
});
}
Runnable net=new Runnable() {
@Override
public void run() {
try {
Socket socket;
//socket=new Socket("192.168.1.102", 12345);//注意这里
socket = new Socket();
SocketAddress socAddress = new InetSocketAddress("192.168.1.102", 12345);
socket.connect(socAddress, 3000);//超时3秒
//发送给服务端的消息
String msg = "Good Night";
try {
//获取输出流并实例化
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
out.write(msg+"\n");//防止粘包
out.flush();//不加这个flush会怎样?
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭Socket
socket.close();
System.out.println("客户端关闭");
}
}
catch (Exception e) {
System.out.println("链接错误");
e.printStackTrace();
}
}
};
从中,可以看到客户端的搭建有以下步骤:

1.创建客户端本身的套接字Socket:

socket = new Socket();

2.建立一个连接(我知道这里和代码不一样,你可能会存疑,但请往下看)

socket = new Socket(ip,port);

3.发送消息:

BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
out.write(msg);
out.flush();//不加这个flush会怎样?
4.关闭客户端Socket:
socket.close();


现在,对于Android的TCP通讯功能还远没有完成,不过快了,先别急,继续往下看:

观察以上android客户端代码。再想想,如何在Activity中使用相关的客户端代码呢?为什么我专门写了一个线程来进行网络操作呢?

答:从4.0开始,安卓就已经不允许在主线程中进行网络相关的操作。这样设计的原因是,由于网络的延迟、不确定性等因素,加之Socket本身在未套接上时是处于阻塞状态的,如果在主线程中进行网络相关操作,就会导致整个app被严重阻塞。

因此,从4.0起,任何尝试在主线程中进行网络操作的动作,都会导致抛出“android.os.NetworkOnMainThreadException”的异常。

那我们该如何解决呢?

创建一个子线程,所以,你会在我给出的代码里看到以下内容:

    Runnable net=new Runnable() {
@Override
public void run() {
//网络操作
}
}
使用时,直接让这个Runnable启动就行了:
new Thread(net).start();


此时你可能会注意到,我在客户端对Socket的实例化,并没有使用大多数人常写的:

socket=new Socket("192.168.1.102", 12345);
而是这样写:

socket = new Socket();
SocketAddress socAddress = new InetSocketAddress("192.168.1.102", 12345);
socket.connect(socAddress, 3000);//超时3秒
为什么呢?我们知道,socket在未连接上时,会一直处于阻塞状态,而此时由于socket本身并未实例化,导致你无法对socket的超时时间进行设置。这往往会导致线程卡主很长一段时间,最后抛出error110(TIME_OUT)。

我这么写,可以人为的设定超时时间,也便于进行多次的超时重播。

另外,android对权限也卡得很死,有些机型或者系统版本根本不允许你的app使用网络。那么我们需要在权限中添加以下内容:

    <!--允许应用程序改变网络状态-->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--允许应用程序改变WIFI连接状态-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<!--允许应用程序访问有关的网络信息-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--允许应用程序访问WIFI网卡的网络信息-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许应用程序完全使用网络-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />

现在可以测试这个实例了。如果你仍然无法成功连接你的PC服务器,接着往下看。


------------------------

>【排错】

从排错的角度来看,我们需要做的,是从服务器和客户端两个方面进行排错。

首先,我们应当确保服务器和端口是没问题的。

在eclipse里打开服务器,然后进入cmd,输入talent localhost 12345。

如果成功,那么eclipse的控制台会输出“客户端已上线”。

然后,我们再测试android客户端连接时输入的PC服务器的ip是没问题的:(ip和port值请自行确定)

重启服务器,进入cmd输入talent 192.168.1.102 12345。

如果这一步成功,但你的android客户端依旧无法连接上你的PC服务器,那么,我想请你检查以下你的windows防火墙的设置。

控制面板\系统和安全\Windows 防火墙 -> 高级设置 ->入站规则 ,看看以下这几项是不是被防火墙禁止了:

Android TCP通信的简单实例以及常见问题[超时/主线程阻塞]

现在再去测试一遍你的客户端,看看是否能连上服务器。大部分的样例代码的超时问题(error:110)在此都可以被解决了。


如果还不行,我们接下来检测安卓客户端。

首先,确保你的手机给了你的app权限。

接着,请检查一遍你设置的端口port值,是不是处于1024以下,或者使用了常用软件及敏感的port的值?如果是,请改成12345再进行测试。

基本上,到了这一步,只要编译没有报错,操作正确,在安卓版本没有发生大的变化的情况下,已经可以连上服务器了。


----------------------

>【理论】

实例结合基础,这里找到了大手子的几篇理论性的文章,可以参考我以上给出这个小实例,再进行深入学习:


Android UDP通信的实现与归纳:

http://blog.csdn.net/shenpibaipao/article/details/70237697

Java输入输出流详解:

http://blog.csdn.net/zsw12013/article/details/6534619

 Socket 通信原理(Android客户端和服务器以TCP&&UDP方式互通):

http://blog.csdn.net/mad1989/article/details/9147661