最近在学一些东西。遇到了不少问题。写个笔记吧,我想。
个人更喜欢着眼于实例,从最简单的开始,一步步进行测试。
理论什么的先放一边,把程序跑起来再说。只有跑起来了,才会有动力去继续往下学,参透整个代码的运行机制。
本次的实例目标是——
模拟一个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()));4.关闭客户端Socket:
out.write(msg);
out.flush();//不加这个flush会怎样?
socket.close();
现在,对于Android的TCP通讯功能还远没有完成,不过快了,先别急,继续往下看:
观察以上android客户端代码。再想想,如何在Activity中使用相关的客户端代码呢?为什么我专门写了一个线程来进行网络操作呢?
答:从4.0开始,安卓就已经不允许在主线程中进行网络相关的操作。这样设计的原因是,由于网络的延迟、不确定性等因素,加之Socket本身在未套接上时是处于阻塞状态的,如果在主线程中进行网络相关操作,就会导致整个app被严重阻塞。
因此,从4.0起,任何尝试在主线程中进行网络操作的动作,都会导致抛出“android.os.NetworkOnMainThreadException”的异常。
那我们该如何解决呢?
创建一个子线程,所以,你会在我给出的代码里看到以下内容:
Runnable net=new Runnable() {使用时,直接让这个Runnable启动就行了:
@Override
public void run() {
//网络操作
}
}
new Thread(net).start();
此时你可能会注意到,我在客户端对Socket的实例化,并没有使用大多数人常写的:
socket=new Socket("192.168.1.102", 12345);而是这样写:
socket = new Socket();为什么呢?我们知道,socket在未连接上时,会一直处于阻塞状态,而此时由于socket本身并未实例化,导致你无法对socket的超时时间进行设置。这往往会导致线程卡主很长一段时间,最后抛出error110(TIME_OUT)。
SocketAddress socAddress = new InetSocketAddress("192.168.1.102", 12345);
socket.connect(socAddress, 3000);//超时3秒
我这么写,可以人为的设定超时时间,也便于进行多次的超时重播。
另外,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 防火墙 -> 高级设置 ->入站规则 ,看看以下这几项是不是被防火墙禁止了:
现在再去测试一遍你的客户端,看看是否能连上服务器。大部分的样例代码的超时问题(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