网络编程学习(一)

前言:最近在学习网络编程只看了一半后面会继续学习,博主写的很清晰很容易理解,博客转自于(http://blog.csdn.net/a10615/article/details/52312009),想去源址学习的朋友可以点击这个链接

TCP 与 UDP 简述

TableTCPUDP
名称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就是请求连接,之后双方通信

Created with Raphaël 2.1.0 client client serverClient serverClient Request Response
2.1、TCP服务器

常用API:

  1. ServerSocket
    1. new ServerSocket(int port) —— 创建监听端口号为port的ServerSocket
    2. getLocalPort() —— 获取本地端口,即Socket监听的端口
    3. setSoTimeout(int timeout) —— 设置accept()的连接超时时间
    4. accept() —— 等待连接。返回客户端的Socket实例对象。若设置了超时,连接超时将抛异常SocketTimeoutException,否则阻塞等待
    5. isClosed() —— 连接是否关闭
    6. close() —— 关闭连接
  2. Socket
    1. setSoTimeout(int timeout) —— 设置read()读取流的超时时间
    2. getInetAddress().getHostAddress() —— 获取客户端的主机IP地址
    3. getPort() —— 获取客户端与本服务器连接端口
    4. getLocalPort() —— 获取本地端口,与serverSocket.getLocalPort()获取的端口一致
    5. getInputStream() —— 获取输入流,用于接收客户端发送过来的信息。一般使用read(byte[] data)来读取,此方法也属于阻塞式,但若设置了读取流的超时时间,超时将抛异常SocketTimeoutException
    6. 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:

  1. Socket
    1. new Socket(String host, int port) —— 创建连接。在90秒内未连接主机,会抛异常ConnectException。可以缩短连接超时时间,使用connect方法
    2. new Socket() —— 创建一个套接字。主要用于connect().
    3. connect(SocketAddress socketAddress, int timeout); —— 开始连接给定的套接字地址,并设置连接超时时间。SocketAddress中封装了主机地址与端口。
    4. setSoTimeout(int timeout) —— 设置读取流的超时时间。必须在getInputStream()与getOutputStream()之前设置才有效
    5. isConnected() —— 是否连接上,返回布尔值
    6. getOutputStream() —— 获取输出流
    7. getInputStream() —— 获取输入流
    8. shutdownOutput() —— 关闭输入流。属于半关闭,而close()属于全关闭
    9. close() —— 关闭连接
  2. SocketAddress
    1. 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),先进行判断是否有数据可读,有就在循环内部读取。这个方法不是阻塞式,如果对方已经把数据发送出来了,这时可以正常读取;如果对方慢了一点,这里就直接执行下去了,数据的读取就被跳过了。当然也可以每次都先等待一段时间再去查。

目前没有找到特别完美又通用的解决方法(如果您有,麻烦分享下),但总有适合的,以下是个人总结的几种解决方法:

  1. 指定协议、添加保文头:报文总数据长度 + 是否继续保持链接 + 当前报文的序号,本报文就根据此长度来判断是否接受完毕;
  2. 设置读入超时时间。如果后面的读取后面的程序还要执行,那就只对读取进行异常处理;
  3. 发送端发送完毕后关闭连接。socket.shutdownOutput()和socket.close()都可以,前者是半关闭,后者是全关闭。前者安全点;
  4. 读取到某些字符后,当成结束。如读到byebye就不读了;
  5. 让每次传输的数据长度都不等于缓冲区buffer大小的整数倍。接收到数据后,判断长度是否为buffer长度。是,则说明还有数据要读;否,则说明已结束;
  6. 用一个足够大的缓冲区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分钟就够了。方法有两个:

  1. 反射去修改默认的保活时间;
    (参考:http://stackoverflow.com/questions/6565667/how-to-set-the-keepalive-timeout-in-android
  2. 在应用层面去定时发送心跳包,实现方法很多,就不多讲了。
2.4、实例:TCP服务器与客户端自由通信

这里的自由通信,指的是服务器端和客服端可以任意的发送和接收数据,不需要一发一收,想发送就发,任意时刻都能接收数据那种

2.4.1:原理分析

发送用的是OutputStream,接收用的是InputStream。一发一收制,发送和接收的都是顺序写到代码中的。现在要自由,无非就是分开来,发送只管发送,接收只管接收。
发送,好处理,需要发送的时候发送就行。
接收,好像没那么容易,因为对方无论何时发送数据,我们必须接收。这里开了个子线程在循环读流。又由于读流是阻塞式,使用的处理方法是上面的第2种,设置读流超时时间。

2.4.2 代码实现

代码包含服务器类SimpleTCPServer与客户端类SimpleTCPClient,及各自的demo。这两个类可以当做简单的工具类使用。
两个工具类对接收的数据使用了不同的处理方法,SimpleTCPServer要求传入Handler进行处理。而SimpleTCPClient是要求调用者实现其抽象方法processData(byte[] data),此方法是在子线程中处理,把更多的处理权交给了调用者。

SimpleTCPServer:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值