资讯专栏INFORMATION COLUMN

重拾Java Network Programming(一)IO流

Lycheeee / 2120人阅读

摘要:不同类型的流入,往往对应于不同类型的流数据。所以通常会将字节缓存到一定数量后再发送。如果是,则将两个标记都抛弃并且将之前的内容作为一行返回。因此二者陷入死锁。因此推出了和类。

前言

最近在重拾Java网络编程,想要了解一些JAVA语言基本的实现,这里记录一下学习的过程。

阅读之前,你需要知道

网络节点(node):位于网络上的互相连通的设备,通常为计算机,也可以是打印机,网桥,路由器等等烧入网卡,从而确保没有任何两台设备的MAC地址是相同的。
IP地址:网络地址,由ISP供应商决定。
包交换网络(packet-switched network):数据被拆解为多个包并分别在线路上传输,每个包包含发送者和接收者的信息。
协议:节点之间进行交流所遵循的规定
网络分层模型

当我们试图访问一个网站时,浏览器实际上直接访问的是本地的传输层(Transport Layer)。传输层将HTTP报文分段为TCP报文并继续向下传递给IP层。IP层封装信息并且将其传送到物理链路上。服务器对收到的数据以相反的顺序解包并读取里面的请求内容。

端口:一台计算机在传输层的每一个协议上通常有65,535个逻辑端口。HTTP通常使用80端口
C/S模型:客户端与服务器模型。通常是客户端向服务器主动发送请求并等待服务器的响应。

基于流的JAVA Socket 通信

Java的IO最初是基于流的。从输入流中读取数据,向输出流中写入数据。不同类型的流入java.io.FileInputStreamsun.net.TelnetOutput Stream往往对应于不同类型的流数据。但是,读取和写入流的方法本质上是相同的。Java还提供了ReadersWriters系列来支持对字符流的输入输出。

流是同步的。同步的流是指当程序(通常是某个线程)要求从流中读取或是向流中写入一段数据时,在获取到数据或是完成数据写入之前,该程序将会一直停在这一步,不会进行任何工作。Java也提供了非阻塞的I/O。我们将在后面继续了解。

OutputStream

先看一下所有输出流的父类OutputStream的API

public abstract class OutputStream{
    //核心方法
    //各个不同的流将实现具体的写入
    //如ByteArrayOutputStream可以直接写入内存,而FileOutputStream则需要根据操作系统调用底层函数来写入文件
    public abstract void write(int b);
    public void write(byte[] data) throws IOException;
    public void write(byte[] data, int offset, int length) throws IOException;

    //将缓存的内容强制写入目标地点
    public void flush() throws IOException;
    public void close() throws IOException;
}

在这里write(int)是核心方法,它会将一个个ASCII码写入输出流中。但是,每写一个字节就传送的浪费是巨大的,因为一次TCP/UDP传输需要携带额外的控制信息和路由信息。所以通常会将字节缓存到一定数量后再发送。

使用完输出流后,及时的关闭它是一个很好的习惯,否则可能会造成资源的浪费或是内存泄漏。我们通常使用finally块来实现:

OutputStream os = null;
try{
    os = new FileOutputStream("/tmp/data.txt");
}catch(IOException e){
    System.err.println(ex.getMessage());
}finally{
    if(os!=null){
        try{
            os.close();
        }catch(IOException e){
            //处理异常
        }
    }
}

上面的代码风格被称为dispose pattern,在使用需要回收资源的类时都会使用这个模式。Java7以后我们可以用另一种更加简洁的方式来实现这个代码:

try(OutputStream os = new FileOutputStream("/tmp/data.txt")){
 ...
}catch(IOException e){
    System.err.println(ex.getMessage());
}

Java会自动调用实现AutoCloseable对象的close()方法,因此我们无需再写ugly的finally块。

InputStream

同样,看一下输入流的基类InputStream的API

public abstract class InputStream{
    //核心方法
    //从输入流中读取一个字节并将其作为int值(0~255)返回
    //当读到输入流末尾时,会返回-1
    public abstract int read() throws IOException;
    public int read(byte[] input) throws IOException;
    public int read(byte[] input, int offset, int length) throws IOException;
    public long skip(long n) throws IOException;
    public int available() throws IOException;
    public void close() throws IOException;
}

输入流也是阻塞式的,它在读取到字节之前会等待,因此其后的代码将不会执行知道读取结束。
为了解决阻塞式读写的问题,可以将IO操作交给多带带的线程来完成。

read()方法返回的应该是一个byte,但是它却返回了int类型,从而导致其返回值变为-128~127之间而不是0~255。我们需要通过一定的转化来获取正确的byte类型:

byte[] input = new byte[10];
for (int i = 0; i < input.length; i++) {
    int b = in.read(); 
    if (b == -1) break; 
    input[i] = (byte) (b>=0? b : b+256);
}

有时候我们无法在一次读取中获得所有的输入,所以最好将读取放在一个循环中,在读取完成之后再跳出循环。

int bytesToRead = 1024;
int bytesRead = 0;
byte[] buffer = new byte[bytesToRead];
while(bytesRead < bytesToRead){
    int tmp = inputStream.read(buffer, bytesRead, bytesToRead-bytesRead);
    //当流结束时,会返回-1
    if(tmp == -1) break;
    bytesRead += tmp;
}
Filters

Java IO采用了装饰者模式,我们可以将一个又一个装饰器加到当前流上,赋予该流新的解析。

在这里,先通过TelnetInputStream从网络上获取Telnet数据流,然后再逐个经过多个过滤器从而获得流中的数据。将过滤器相连的方法很简单:

FileInputStream fin = new FileInputStream("data.txt"); BufferedInputStream bin = new BufferedInputStream(fin);

Buffered Stream
先将数据缓存至内存,再在flush或是缓存满了以后写入底层。
大多数情况下,缓存输出流可以提高性能,但是在网络IO的场景下不一定,因为此时的性能瓶颈取决于网速,而不是网卡将数据传到上层的速度或是应用程序运行的速度。

构造器如下:

public BufferedInputStream(InputStream in);
public BufferedInputStream(InputStream in, int bufferSize);
public BufferedOutputStream(OutputStream out);
public BufferedOutputStream(OutputStream out, int bufferSize);

PrintStream
输出流,System.out就是一个PrintStream。默认情况下,我们需要强制flush将PrintStream中的内容写出。但是,如果我们在构造函数中将自动flush设置为true,则每次写入一个byte数组或是写入换行符或是调用println操作都会flush该流。

public PrintStream(OutputStream out)
public PrintStream(OutputStream out, boolean autoFlush)

但是,在网络环境中应当尽可能不使用PrintStream,因为在不同的操作系统上,println的行为不同(因为换行符的标记不同)。因此同样的数据在不同的操作系统上可能不一致。

除此以外,PrintStream依赖于平台的默认编码。但是,这个编码和服务器端期待的编码格式很可能是不一样的。PrintStream不提供改变编码的接口。

而且,PrintStream会吞掉所有的异常。我们无法对异常进行相应的编码。

Date Stream
以二进制数据的格式读取和写入Java的基本数据类型和String类型。

DataOutputStream的API:

public final void writeBoolean(boolean b) throws IOException 
public final void writeByte(int b) throws IOException 
public final void writeShort(int s) throws IOException 
public final void writeChar(int c) throws IOException 
public final void writeInt(int i) throws IOException
public final void writeLong(long l) throws IOException 
public final void writeFloat(float f) throws IOException 
public final void writeDouble(double d) throws IOException 

//根据UTF-16编码将其转化为长度为两个字节的字符
public final void writeChars(String s) throws IOException 

//只存储关键的信息,任何超出Latin-1编码范围的内容都将会丢失
public final void writeBytes(String s) throws IOException 

//上面两个方法都没有将字符串的长度写入输出流,所以无法分辨究竟原始字符还是构成字符串的最终字符
//该方法采用UTF-8格式编码,并且记录的字符串的长度
//它应当只用来和其它Java的程序交换信息
public final void writeUTF(String s) throws IOException

DataInputStream的API:

public final boolean readBoolean() throws IOException 
public final byte readByte() throws IOException 
public final char readChar() throws IOException 
public final short readShort() throws IOException 
public final int readInt() throws IOException
public final long readLong() throws IOException 
public final float readFloat() throws IOException 
public final double readDouble() throws IOException 
public final String readUTF() throws IOException

//读取别的程序写的unsigned类型数据,如C
public final int readUnsignedByte() throws IOException 
public final int readUnsignedShort() throws IOException

public final int read(byte[] input) throws IOException 
public final int read(byte[] input, int offset, int length) throws IOException

//完整的读取一定长度的字符串,如果可读的长度不足,将抛出IOException
//可用于已知读取长度的场景,如读取HTTP报文。
我们可以使用Header中的content-length属性来读取相应长度的body
public final void readFully(byte[] input) throws IOException
public final void readFully(byte[] input, int offset, int length) throws IOException

//读取一行 但是最好不要使用,因为它无法正确的将非ASCII码转化为字符串
public final String readLine() throws IOException

这里需要强调一下为什么不要使用readLine()方法。因为readLine方法识别一行末尾的方法是通过 或是 。当readLine遇到 时,它会判断下一个字符是不是 。如果是,则将两个标记都抛弃并且将之前的内容作为一行返回。如果不是,则抛弃 并将之前的内容返回。问题在于,如果流中最后一个字符为 ,那么读取一行的方法会挂起,并等待下一个字符。

这个问题在网络IO中特别明显,因为当一次数据发送结束之后,客户端在关闭连接之前会等待服务器端的响应。服务器端却在等待一个不存在的输入。因此二者陷入死锁。如果幸运的话,客户端会因为超时断开连接,使得死锁结束,同时你丢失了最后一行数据。也有可能这个程序无限死锁下去。

Readers Writers

文本中的字符并不能和ASCII码完全划等号。很多国家的语言如中文,日文,韩文等都远远超出了ASCII码编码的范围。用ASCII码是无法识别这些字节的。因此JAVA推出了Reader和Writer类。它将根据特定的编码来解读字节。

Writer类API

protected Writer()
protected Writer(Object lock)
public abstract void write(char[] text, int offset, int length) throws IOException
public void write(int c) throws IOException
public void write(char[] text) throws IOException
public void write(String s) throws IOException
public void write(String s, int offset, int length) throws IOException public abstract void flush() throws IOException
public abstract void close() throws IOException

这里和之前的区别在于将根据选择的编码转化为相应的byte。

OutputStreamWriter
将字节流根据选择的编码转化为字符流。

public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException
public void write(String s) throws IOException;

Reader类API

protected Reader()
protected Reader(Object lock)
public abstract int read(char[] text, int offset, int length) throws IOException

//返回unicode对应的0~65535之间的整数
//如果到达了流的末尾,则返回-1
public int read() throws IOException
public int read(char[] text) throws IOException 

//跳过n个字符
public long skip(long n) throws IOException 
public boolean ready()
public boolean markSupported()

//设置标记与重置下标至标记处
public void mark(int readAheadLimit) throws IOException 
public void reset() throws IOException
public abstract void close() throws IOException

InputStreamReader

public InputStreamReader(InputStream in)

//如果没有可以匹配的编码,则抛出UnsupportedEncodingException
public InputStreamReader(InputStream in, String encoding)
throws UnsupportedEncodingException

使用InputStreamReader的范例:

public static String getMacCyrillicString(InputStream in) throws IOException {
    InputStreamReader r = new InputStreamReader(in, "MacCyrillic");            
    StringBuilder sb = new StringBuilder();
    int c;
    while ((c = r.read()) != -1) sb.append((char) c);
    return sb.toString();
}

PrintWriter
PrintStream的字符形式阅读,尽量使用PrintWriter而非PrintStream因为正如前面提到的,PrintStream依赖于当前平台的编码,并且无法修改。

public PrintWriter(Writer out)
public PrintWriter(Writer out, boolean autoFlush) 
public PrintWriter(OutputStream out)
public PrintWriter(OutputStream out, boolean autoFlush) 
public void flush()
public void close()
public boolean checkError()
public void write(int c)
public void write(char[] text, int offset, int length) 
public void write(char[] text)
public void write(String s, int offset, int length) 
public void write(String s)
public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long l)
public void print(float f)
public void print(double d)
public void print(char[] text)
public void print(String s)
public void print(Object o)
public void println()
public void println(boolean b)
public void println(char c)
public void println(int i)
public void println(long l)
public void println(float f)
public void println(double d)
public void println(char[] text)
public void println(String s)
public void println(Object o)
参考书籍
Java Network Prograing 4th edition
HTTP权威指南


想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76348.html

相关文章

  • 重拾Java Network Programming(四)URLConnection & C

    摘要:从而一方面减少了响应时间,另一方面减少了服务器的压力。表明响应只能被单个用户缓存,不能作为共享缓存即代理服务器不能缓存它。这种情况称为服务器再验证。否则会返回响应。 前言 本文将根据最近所学的Java网络编程实现一个简单的基于URL的缓存。本文将涉及如下内容: HTTP协议 HTTP协议中与缓存相关的内容 URLConnection 和 HTTPURLConnection Respo...

    Guakin_Huang 评论0 收藏0
  • 重拾Java Network Programming(四)URLConnection & C

    摘要:从而一方面减少了响应时间,另一方面减少了服务器的压力。表明响应只能被单个用户缓存,不能作为共享缓存即代理服务器不能缓存它。这种情况称为服务器再验证。否则会返回响应。 前言 本文将根据最近所学的Java网络编程实现一个简单的基于URL的缓存。本文将涉及如下内容: HTTP协议 HTTP协议中与缓存相关的内容 URLConnection 和 HTTPURLConnection Respo...

    魏明 评论0 收藏0
  • 重拾Java Network Programming(二)InetAddress

    摘要:前言今天,我将梳理在网络编程中很重要的一个类以及其相关的类。这类主机通常不需要外部互联网服务,仅有主机间相互通讯的需求。可以通过该接口获取所有本地地址,并根据这些地址创建。在这里我们使用阻塞队列实现主线程和打印线程之间的通信。 前言 今天,我将梳理在Java网络编程中很重要的一个类InetAddress以及其相关的类NetworkInterface。在这篇文章中将会涉及: InetA...

    daryl 评论0 收藏0
  • 重拾JAVA线程之获取另个线程的返回

    摘要:它将管理线程的创建销毁和复用,尽最大可能提高线程的使用效率。如果我们在另一个线程中需要使用这个结果,则这个线程会挂起直到另一个线程返回该结果。我们无需再在另一个线程中使用回调函数来处理结果。 前言 Java的多线程机制允许我们将可以并行的任务分配给不同的线程同时完成。但是,如果我们希望在另一个线程的结果之上进行后续操作,我们应该怎么办呢? 注:本文的代码没有经过具体实践的检验,纯属为了...

    liuchengxu 评论0 收藏0
  • JAVA NIO 步步构建I/O多路复用的请求模型

    摘要:为解决这问题,我们发现元凶处在一线程一请求上,如果一个线程能同时处理多个请求,那么在高并发下性能上会大大改善。这样一个线程可以同时发起多个调用,并且不需要同步等待数据就绪。表示当前就绪的事件类型。 JAVA NIO 一步步构建I/O多路复用的请求模型 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 文章一:JAVA ...

    X_AirDu 评论0 收藏0

发表评论

0条评论

Lycheeee

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<