前言:最近在学习网络编程只看了一半后面会继续学习,博主写的很清晰很容易理解,博客转自于(http://blog.csdn.net/a10615/article/details/52312009),想去源址学习的朋友可以点击这个链接
TCP 与 UDP 简述
Table | TCP | UDP |
---|---|---|
名称 | Transmission Control Protocol,传输控制协议 | User Datagram Protocol,用户数据报协议 |
连接方式 | 面向连接(收发数据前,必须与对方建立可靠连接) | 面向非连接 |
握手 | 3次 | 无 |
可靠性 | 可靠 | 不可靠 |
效率 | 慢 | 快 |
应用 | 可靠性高、数据量大的场合 | 快速、数据量小的场合 |
TCP、UDP通信:需要知道IP 以及端口
在编程时,注意以下几点:
- 端口号应该大于等于1024,因为0~1023内都被系统内部占用了。
- 在清单文件中添加权限
<uses-permission android:name="android.permission.INTERNET" />
- 在Android中,TCP和UDP都是网络连接,属于耗时操作,所以必须放在子线程中。
二、TCP
TCP的是socket分为ServerSocket(服务器) 和 Socket(客户端),ServerSocket一直处于监听状态,方便客户端随时连接进来,而socket就是请求连接,之后双方通信
2.1、TCP服务器
常用API:
- ServerSocket
- new ServerSocket(int port) —— 创建监听端口号为port的ServerSocket
- getLocalPort() —— 获取本地端口,即Socket监听的端口
- setSoTimeout(int timeout) —— 设置accept()的连接超时时间
- accept() —— 等待连接。返回客户端的Socket实例对象。若设置了超时,连接超时将抛异常SocketTimeoutException,否则阻塞等待
- isClosed() —— 连接是否关闭
- close() —— 关闭连接
- Socket
- setSoTimeout(int timeout) —— 设置read()读取流的超时时间
- getInetAddress().getHostAddress() —— 获取客户端的主机IP地址
- getPort() —— 获取客户端与本服务器连接端口
- getLocalPort() —— 获取本地端口,与serverSocket.getLocalPort()获取的端口一致
- getInputStream() —— 获取输入流,用于接收客户端发送过来的信息。一般使用read(byte[] data)来读取,此方法也属于阻塞式,但若设置了读取流的超时时间,超时将抛异常SocketTimeoutException
- getOutputStream() —— 获取输出流,用户给客户端发送信息
这里是一个简单的服务器实例,只实现一次请求,然后响应一次即完毕:
private void startTCPServer() {
final int port = 8989;
new Thread(new Runnable() {
@Override
public void run() {
ServerSocket server = null;
try {
// 1、创建ServerSocket服务器套接字
server = new ServerSocket(port);
// 设置连接超时时间,不设置,则是一直阻塞等待
server.setSoTimeout(8000);
// 2、等待被连接。在连接超时时间内连接有效,超时则抛异常,
Socket client = server.accept();
logD("connected...");
// 设置读取流的超时时间,不设置,则是一直阻塞读取
client.setSoTimeout(5000);
// 3、获取输入流和输出流
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
// 4、读取数据
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String receData = new String(buf, 0, len, Charset.forName("UTF-8"));
logD("received data from client: " + receData);
// 5、发送响应数据
byte[] responseBuf = "Hi, I am Server".getBytes(Charset.forName("UTF-8"));
outputStream.write(responseBuf, 0, responseBuf.length);
} catch (IOException e) {
logD("Exception:" + e.toString());
e.printStackTrace();
} finally {
if (server != null) {
try {
server.close();
server = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
2.2、TCP客户端
常用API:
- Socket
- new Socket(String host, int port) —— 创建连接。在90秒内未连接主机,会抛异常ConnectException。可以缩短连接超时时间,使用connect方法
- new Socket() —— 创建一个套接字。主要用于connect().
- connect(SocketAddress socketAddress, int timeout); —— 开始连接给定的套接字地址,并设置连接超时时间。SocketAddress中封装了主机地址与端口。
- setSoTimeout(int timeout) —— 设置读取流的超时时间。必须在getInputStream()与getOutputStream()之前设置才有效
- isConnected() —— 是否连接上,返回布尔值
- getOutputStream() —— 获取输出流
- getInputStream() —— 获取输入流
- shutdownOutput() —— 关闭输入流。属于半关闭,而close()属于全关闭
- close() —— 关闭连接
- SocketAddress
- new InetSocketAddress(host, port) —— SocketAddress是抽象类,使用SocketAddress子类来实例化。把主机IP地址与端口封装进去。
客户端代码也是最简单的实例,一发一收即结束。
private void startTCPClient() {
final String host = “192.168.1.214”;
final int port = 8989;
new Thread(new Runnable() {
@Override
public void run() {
Socket socket = null;
try {
// 1、创建连接
socket = new Socket(host, port);
if (socket.isConnected()) {
logD("connect to Server success");
}
// 2、设置读流的超时时间
socket.setSoTimeout(8000);
// 3、获取输出流与输入流
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
// 4、发送信息
byte[] sendData = "Hello, I am client".getBytes(Charset.forName("UTF-8"));
outputStream.write(sendData, 0, sendData.length);
outputStream.flush();
// 5、接收信息
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String receData = new String(buf, 0, len, Charset.forName("UTF-8"));
logD(receData);
} catch (IOException e) {
e.printStackTrace();
logD(e.toString());
} finally {
if (socket != null) {
try {
socket.close();
socket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
2.3、TCP总结
2.3.1 读取流阻塞的处理
因为上面的demo中,都是假设对方发送的数据长度小于等于1024。可是现实中数据的长度一般都是未知的,很多人用读取文件的方法来读取,如:
byte[] buf = new byte[1024];
StringBuilder rece = new StringBuilder();
int len;
while ((len = inputStream.read(buf)) != -1) {
String part = new String(buf, 0, len);
rece.append(part);
}
String receData = rece.toString();
这样,看上去很好,但实际socket的流与文件流不同,文件读到末尾,有一个结束标记。而socket的流只有在关闭流的时候,才会有被告知结束。这里的结束就是read()返回的长度等于-1。
TCP通信时,一般是不会立马关闭,而read()又是阻塞式方法,所以会导致程序一直停留在这里while ((len = inputStream.read(buf)) != -1)
。
首先,有两种不太靠谱的解决方法:
1. 不要用-1作为判断条件,而用>0。但我这结果都一样
2. 使用BufferedReader的readLine()去读取数据,这是读行。那如果我的数据里没有换行,怎么读?传文件很好,但绝大多数情况下不适合。
BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
String receData = bufReader.readLine();
稍微靠谱点的方法:
使用while(inputStream.available() > 0)
,先进行判断是否有数据可读,有就在循环内部读取。这个方法不是阻塞式,如果对方已经把数据发送出来了,这时可以正常读取;如果对方慢了一点,这里就直接执行下去了,数据的读取就被跳过了。当然也可以每次都先等待一段时间再去查。
目前没有找到特别完美又通用的解决方法(如果您有,麻烦分享下),但总有适合的,以下是个人总结的几种解决方法:
- 指定协议、添加保文头:报文总数据长度 + 是否继续保持链接 + 当前报文的序号,本报文就根据此长度来判断是否接受完毕;
- 设置读入超时时间。如果后面的读取后面的程序还要执行,那就只对读取进行异常处理;
- 发送端发送完毕后关闭连接。socket.shutdownOutput()和socket.close()都可以,前者是半关闭,后者是全关闭。前者安全点;
- 读取到某些字符后,当成结束。如读到byebye就不读了;
- 让每次传输的数据长度都不等于缓冲区buffer大小的整数倍。接收到数据后,判断长度是否为buffer长度。是,则说明还有数据要读;否,则说明已结束;
- 用一个足够大的缓冲区buffer来一次性装完所有的数据。
2.3.2 流的处理
个人还是喜欢用byte,可能因为以前使用C的原因,再加上项目中也是字节数据+字符数据。很多情况下,我们都是字符串数据或对象实例之类的,可以用封装类进行处理,如BufferedWriter/BufferedReader、ObjectInputStream/ObjectOutputStream、DataInputStream/DataOutputStream与PrintWriter。
对象流就不写demo了,需注意的是:
- 被读写的对象必须实现Serializable;
- 读写顺序必须一致。
DataInputStream+DataOutputStream:
// 发送
DataOutputStream dataOS = new DataOutputStream(outputStream);
dataOS.writeUTF(“Hi你好啊, I am Server”);
dataOS.flush();
dataOS.close(); // 如果发送后不再接收或发送,就可以关闭,否则
不要关闭。因为这样也会把socket关闭,导致无法再接收或发送了,并抛SocketException异常。若不再发送,只接收,可用socket.shutdownOutput();
// 接收
DataInputStream dataIS = new DataInputStream(inputStream);
String receData = dataIS.readUTF();
logD(“From Client:” + receData);
PrintWriter + BufferedReader:
// 发送
// 使用带自动flush刷新的构造函数,自动刷新仅对这三个方法有效:println, printf, format
PrintWriter printWriter = new PrintWriter(outputStream, true);
printWriter.format(“First info from client = %s”, host);
printWriter.printf(“Second info: Android”);
socket.shutdownOutput();
// 接收
// 这里可以一次性把上面两次的写入流读出来。
BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
char[] buffer = new char[1024];
int length;
if ((length = bufReader.read(buffer)) != -1) {
String receData = new String(buffer, 0, length);
logD(“From Client:” + receData);
}
2.3.3 TCP心跳保活
心跳是双方约定好保活时间,在此保活时间内告诉对方自己还在线,不要关闭连接。对方在保活时间内没有收到保活信息,就会关闭连接。
正常情况下,TCP有默认的保活时间,为2小时,可在客户端开启保活功能socket.setKeepAlive(true);
。
但我们并不需要那么长的保活时间,一般10分钟就够了。方法有两个:
- 反射去修改默认的保活时间;
(参考:http://stackoverflow.com/questions/6565667/how-to-set-the-keepalive-timeout-in-android) - 在应用层面去定时发送心跳包,实现方法很多,就不多讲了。
2.4、实例:TCP服务器与客户端自由通信
这里的自由通信,指的是服务器端和客服端可以任意的发送和接收数据,不需要一发一收,想发送就发,任意时刻都能接收数据那种
2.4.1:原理分析
发送用的是OutputStream,接收用的是InputStream。一发一收制,发送和接收的都是顺序写到代码中的。现在要自由,无非就是分开来,发送只管发送,接收只管接收。
发送,好处理,需要发送的时候发送就行。
接收,好像没那么容易,因为对方无论何时发送数据,我们必须接收。这里开了个子线程在循环读流。又由于读流是阻塞式,使用的处理方法是上面的第2种,设置读流超时时间。
2.4.2 代码实现
代码包含服务器类SimpleTCPServer与客户端类SimpleTCPClient,及各自的demo。这两个类可以当做简单的工具类使用。
两个工具类对接收的数据使用了不同的处理方法,SimpleTCPServer要求传入Handler进行处理。而SimpleTCPClient是要求调用者实现其抽象方法processData(byte[] data),此方法是在子线程中处理,把更多的处理权交给了调用者。
SimpleTCPServer: