Java网络编程
第一章 流
前言
网络程序所做的很大一部分工作都是简单的输入和输出:将数据字节从一个系统移动到另一个系统。
一、Java中的流?
Java的I/O建立于流(stream)。输入流读取数据;输出流写入数据。例如:java.io.FileInputStream和sun.net.TelnetOutputStream会读/写某个特定的数据。
tips:不知道大家看到input是输入流可能会自觉认为使用该流是将数据输入到某个文件,其实结果恰恰相反,输入流是读取数据,输出流才是将数据放入文件。这里给出我的理解,我们把input和output都当作是“java”是主题,input 即 往java程序中输入数据,output及java向外释放数据。哈哈哈
二、使用步骤
1.输出流
Java的基本输出流类是java.io.OutputStream: public abstract class OutputStream
其方法有:
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
public void flush() throws IOException {}
public void close() throws IOException {}
下面以public abstract void write(int b) throws IOException; 为例子向文件out.txt.中每行写入72个字符(字符范围33-126之间),虽说这个方法收到的参数是int类型的值,却向文件中输入的是其int对应的ASCII值的字符。其实从其参数只能输入0-255的int,超出部分,将写入这个数的最低字节。(这里的不好理解,在下面有代码和详细解释)
public class GenerateCharacters {
public static void generate(OutputStream out) throws IOException {
int firstPrintableC = 33;
int numberOfPrintableC = 94;
int numberOfCPerLine = 72;
int start = firstPrintableC;
//注意为了方便,这里写了死循环。注意自行切断程序。
while (true){
for (int i = start; i < start + numberOfCPerLine; i++) {
out.write(((i - firstPrintableC) % numberOfPrintableC) + firstPrintableC);
}
out.write('\r');
out.write('\n');
start = ((start + 1) - firstPrintableC) % numberOfPrintableC + firstPrintableC;
}
}
}
@GetMapping("/input")
public Integer input_file(){
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
GenerateCharacters.generate(fos);
} catch (Exception e) {
e.printStackTrace();
}
return 1;
}
写入效果:
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
ASCII图:
如果输入数据大于255会发生什么?
在二进制中,256可以表示为100000000(注意这里是一个9位的二进制数,因为256是2的8次方加1,所以需要一个额外的位来表示)。但是,因为write方法只接受一个字节(8位)的数据,所以只有这个数的最低8位会被考虑。然而,由于256的最低8位在二进制中实际上是00000000(如果你只看最低的8位,并忽略溢出的最高位),这会导致写入的值实际上是0。 然而,有一个更简单且准确的方式来理解这个行为:当你将任何int值传递给write(int b)方法时,Java都会将这个int值模256(即b % 256),然后将结果转换为无符号8位整数并写入。
使用write(int n) 的问题很明显
一次只能写入一个字节,效率太低。流出以太网卡的每个TCP分片包含至少40字节的开销用于路由和纠错。如果每个字节都单独发送,那么开销巨大。因此大多数TCP/IP实现都会在某种程度上缓存数据,在内存中累计字节,到一定量的数据后,或经过一定时间后,才将积累的数据发送。所以使用write(byte[] data) 或者write(byte b[], int off, int len)比较常用
下面使用write(byte[] data)改写上述代码:
public static void generateCs(OutputStream out) throws IOException {
int firstPrintableC = 33;
int numberOfPrintableC = 94;
int numberOfCPerLine = 72;
int start = firstPrintableC;
byte[] line = new byte[numberOfCPerLine + 2]; //+2对应回车 和换行符号
while (true){
for (int i = start; i < start + numberOfCPerLine; i++) {
line[i-start] = (byte)((i-firstPrintableC) % numberOfPrintableC +firstPrintableC);
}
line[72] = (byte) '\r';
line[73] = (byte) '\n';
out.write(line);
start = ((start + 1) - firstPrintableC) % numberOfPrintableC + firstPrintableC;
}
}
效果是一样的,先把一行的数据放入line[]中,然后一次写入一行。
2.输入流
Java的基本输出流类是java.io.InputStream: public abstract class InputStream
其方法有:
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
public long skip(long n) throws IOException {
long remaining = n;
int nr;
if (n <= 0) {
return 0;
}
int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
byte[] skipBuffer = new byte[size];
while (remaining > 0) {
nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
if (nr < 0) {
break;
}
remaining -= nr;
}
return n - remaining;
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {}
InputStream的具体子类使用这些方法从某种具体的介质中读取数据。例如:FileInputStream从文件中读取数据。TelnetInputStream从网络连接中读取数据。ByteArrayInputStram从字节数组中读取数据。无论哪种介质,均使用以上6种方法。 多态的作用: 子类的实例可以透明地作为其超类的实例来使用。并不需要子类的特定知识。
下面用FileInputStream读取文件
public static void inputC() throws IOException{
String filePath = "output.txt"; // 文件的路径
try (InputStream inputStream = new FileInputStream(filePath)) {
int data;
// 使用一个缓冲区来存储读取的字节数据
byte[] buffer = new byte[5];
int bytesRead = 0;
// 循环读取文件内容,直到文件末尾
while ((bytesRead = inputStream.read(buffer)) != -1) {
// 将读取的字节数组转换为字符串并打印
int length = Math.min(bytesRead, buffer.length);
String text = new String(buffer, 0, length);
System.out.println(text);
}
} catch (IOException e) {
// 捕获并打印IO异常
e.printStackTrace();
}
}
使用5个字节的缓冲区,进行文件内容读取。这样又能实践到缓冲区知识,也可完成input流知识实践。 结果如下,使用5字节缓冲区读取文件内容打印执行完毕速度要慢于72字节缓冲区速度。 read()方法会等待并阻塞后面代码的执行,直到读取到1个字节的数据。如果有模拟管道的场景,尽量要将I/O放在单独的线程中。
$%&'(
)*+,-
./012
34567
89:;
流的其他函数使用
在网络数据传输场景,网络比CPU慢很多,所以程序很容易在所有数据到达前清空缓冲区。 所有3个read()方法结束都会以-1表示。 有一种可能是字节永远没有到达,可以使用avaliable()方法来确定不阻塞的情况下有多少字节可以读取。它会返回读取最少字节数量。
在少数情况下,需要跳过一些数据不读取,则使用skip() 与输入流一样,输出流一旦结束,应调用它close()方法将其关闭。这会释放与这个流关联的所有资源,如句柄或端口。
总结
以上就是今天要讲的内容,本文仅仅简单介绍了java输入输出流的使用与浅浅的分析,而合理使用其函数可以减少I/O读取次数,优化程序。