Java基础—网络编程(一)

网络编程(一)

一、网络模型

这里写图片描述

1.OSI参考模型
OSI网络模型的每一层,都有自己特有的数据封装特征信息。两台主机通信,首先在应用层将数据封装。将会给数据加上应用层的特征信息,传递给OSI网络模型的下一层表示层;表示层也会给数据加上自己特有的特征;以此类推。到传输层(TCP/IPUDP协议所在层),加上TCP的特征信息,交由网络层;网络层将IP协议赋给该数据包,标识该数据包的目的地,再交给数据链路层;数据链路层标识该数据包的传输方式,交给最后一层物理层(网线,光纤,无线设备);这时,数据封包结束,通过物理层传输到目的主机的物理层,由对方主机,从底层物理层开始,对数据进行数据拆包。

2.TCP/IP参考模型
TCP/IP参考模型分为四个层次。它将OSI参考模型的应用层、表示层和会话层合并为应用层,保留了传输层和网际层,最后将数据链路层和物理层合并为主机至网络层。

注意:
把用户应用程序最为最高层,把物理通信线路最为最底层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。网络模型的每个层次都有自己的传输协议。传输层的协议是TCP和UDP协议;网际层最常用的是IP协议;应用层通常使用HTTP/FTP协议

二、网络通信三要素

1.对方IP地址
网络通信的首要素是要知道对方的IP地址,确定对方的主机在网络上的位置,才能发起进一步的请求。

2.应用端口
网络通信第二要素是获取对方的端口。要将数据要发送到指定的应用程序上,就必须直到对方用于接收本方数据的应用服务端口。每个网络应用程序都有一个网络端口用于接收数据。主机上的端口可以在0-65535之间任取,但是0-1024默认作为系统端口,不建议使用。

3.通信协议
通信协议,即通信规则。国际组织定义了一个在传输层和网络层通用的协议————TCP/IP协议

三、IP地址

1.概述
IP(Internet Protocol)协议是网际层的主要协议,支持网间互连的数据包通信。它提供无连接数据包传送,数据包路由选择和差错控制。IP地址分4段,每段为1个字节,每段最大值为255IP地址用于确定主机在网络中的位置。

2、本地回环地址
127.0.0.1是本地回环地址,可以用于测试网卡工作是否正常或这用于测试本地网页等。

3.局域网IP地址
192.168开头的IP被保留,不用于公网,用于局域网。其中有一个例外,第四个字段为255的被保留作广播IP地址,给这个IP发送的消息,局域网内所有主机都能接收。

4.子网掩码
子网掩码用于划分局域网。由于主机数量越来越多,出现了IP不够的情况。划分子网,让多台主机使用同一个公网IP,部分解决了这一问题。

5.域名
IP地址有4段,不易记忆,通常给主机起一个名字,如127.0.0.1称为localhost,这就是127.0.0.1这个IP地址的域名

四、通信协议

1.UDP协议
1)概述
UDP(user data protocol)向应用程序提供了一种发送封装的原始IP数据包的方法。该协议面向无连接,通信主机一端只负责发送数据,不负责对方是否接收到数据;如果对方处于在线状态,就能接收到数据包;如果不在线,该数据包就丢失。UDP在传输数据时,两通信主机不一定已经建立连接。

2)特点
1> 将数据、数据源和目的封装成数据包;
2> 数据包大小有限制,限制在64k以内;
3> 不需要建立连接,协议不可靠
4> 速度快,效率高

2.TCP协议
1)概述
TCP(transmission control protocol)是专门设计用于在不可靠的因特网上提供可靠的、端到端的字节流通讯协议。它是一种面向连接的协议。TCPUDP协议相反,是面向连接的协议。该协议在传输数据前必须先建立连接。这个连接的建立是基于三次握手的机制之上的。三次握手确保了数据连接通路的存在,一旦通信双方有一方断开连接,数据传输即刻停止。

2)特点
1> 数据传输大小无限制
2> 通过三次握手机制确定连接通路畅通
3> 必须建立连接,协议可靠
4> 速度较慢,效率较低

五、网络编程常用类

1.Socket类
1)概述
网络编程其实就是Socket编程Socket是为网络服务提供的一种机制,是通信得以进行的前提。通信两端的主机都有Socket,两端通信其实就是Socket间在通信,数据在两个Socket间通过IO进行传输。TCPUDP协议,都是基于Socket进行数据传输。

2)不同协议的Socket创建
每个传输协议都有自己建立传输端点的方式。下面进行不同协议的Socket服务的建立。

  • UDP协议建立发送端Socket
    DatagramSocket和DatagramPacket类
    DatagramSocket类表示用来发送和接收数据包的套接字(Socket)。该类既能发送,又能接收。DatagramSocket在发送数据时,提供了数据包对象DatagramPacket,用于数据封包。

第一步:
建立发送端UDPSocket服务

/* 通过DatagramSocket对象创建UDP Socket服务 */
DatagramSocket ds = new DatagramSocket();

第二步:
提供数据,并将数据封装到数据包中

/* 通过DatagramPacket对象创建数据包对象 */
byte[] buf = "UDP ge men er lai la !~~#!@$@%#^$&".getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("localhost"), 7777);

第三步:
通过Socket服务发送数据包

/* 调用send()方法,通过Socket服务发送数据包 */
ds.send(dp);

第四步:
关闭资源

/* 关闭资源 */
ds.close();
  • UDP协议建立接收端Socket

第一步:
建立接收端UDPSocket服务

/* 通过DatagramSocket建立接收端Socket服务,并监听相应端口*/
/* 定义监听端口很重要,发送端会将数据发送到7777端口,如果补监听,收不到数据 */
DatagramSocket ds = new DatagramSocket(7777);

第二步:
定义数据包对象

/* 创建数据包对象,用于存储数据 */
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);

第三步:
通过Socket服务接收数据包

/* 调用reveive()方法接收数据包 */
ds.receive(dp);

第四步:
解析数据包

/* 解析数据,获取地址,数据,长度等*/
String ip = dp.getAddress().getHostAddress();
String data = new String(dp.getData(), 0, dp.getLength());
int port = dp.getPort();
System.out.println(ip + "::" + data + "::" + port);

第五步:
关闭资源

/* 关闭资源 */
ds.close();

程序运行结果:127.0.0.1::UDP ge men er lai la !~~#!@$@%#^$&::64979

注意:
UDP接收端的receive()方法是阻塞式方法

完整示例代码:

package com.heisejiuhuche.socket;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class DatagramSendSocketDemo {
    public static void main(String[] args) throws IOException {
        /* 通过DatagramSocket对象创建UDP Socket服务 */
        DatagramSocket ds = new DatagramSocket();

        /* 通过DatagramPacket对象创建数据包对象 */
        byte[] buf = "UDP ge men er lai la !~~#!@$@%#^$&".getBytes();
        DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("localhost"), 7777);

        /* 调用send()方法,通过Socket服务发送数据包 */
        ds.send(dp);

        /* 关闭资源 */
        ds.close();
    }
}

package com.heisejiuhuche.socket;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class DatagramRecvSocketDemo {
    public static void main(String[] args) throws IOException {
        /* 通过DatagramSocket建立接收端Socket服务,并监听相应端口*/
        DatagramSocket ds = new DatagramSocket(7777);

        /* 创建数据包对象,用于存储数据 */
        byte[] buf = new byte[1024];
        DatagramPacket dp = new DatagramPacket(buf, buf.length);
        /* 调用reveive()方法接收数据包 */
        ds.receive(dp);
        /* 解析数据,获取地址,数据,长度等*/
         String ip = dp.getAddress().getHostAddress();
         String data = new String(dp.getData(), 0, dp.getLength());
         int port = dp.getPort();
         System.out.println(ip + "::" + data + "::" + port);
        /* 关闭资源 */
        ds.close();
    }
}

练习1:
键盘录入方式发送数据

示例代码:

package com.heisejiuhuche.socket;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class SendFromKeyDemo {
    public static void main(String[] args) throws Exception {
        /* 创建DatagramSocket对象 */
        DatagramSocket ds = new DatagramSocket();
        /* 创建缓冲流对象,接收键盘录入 */
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        /* 接收录入,如果录入over,结束程序;如果不是,将录入的内容发送出去 */
        while((line = bufr.readLine()) != null) {
            if(line.equals("886"))
                break;
            byte[] buf = line.getBytes();
            DatagramPacket dp = new DatagramPacket(
                    buf, buf.length, InetAddress.getByName("localhost"), 10000);
            ds.send(dp);
        }
        /* 关闭资源 */
        ds.close();
    }
}

package com.heisejiuhuche.socket;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ConstantRecvDemo {
    public static void main(String[] args) throws Exception {
        /* 创建DatagramPacket对象用于接收数据,并监听相应端口 */
        DatagramSocket ds = new DatagramSocket(10000);
        /* 创建缓冲区数组 */
        byte[] buf = new byte[1024];
        /* 调用receive方法获取数据并解析 */
        while(true) {
            DatagramPacket dp = new DatagramPacket(buf, buf.length);
            ds.receive(dp);
            String IP = dp.getAddress().getHostAddress();
            String data = new String(dp.getData(), 0, dp.getLength());
            System.out.println(IP + ":" + data);
        }
    }
}

练习2:
使用多线程技术,编写聊天小程序。

示例代码:

package com.heisejiuhuche.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.BindException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

public class ChatDemo {
    public static void main(String[] args) {
        try {
            /* 启动发送和接收线程 */
            DatagramSocket dsSend = new DatagramSocket();
            DatagramSocket dsRecv = new DatagramSocket(10002);
            new Thread(new Client(dsSend)).start();
            new Thread(new Server(dsRecv)).start();
        } catch(BindException e) {
            throw new RuntimeException("Port in use...");
        } catch(SocketException e) {
            throw new RuntimeException("Socket create failed....");
        }
    }
}

class Client implements Runnable {
    private DatagramSocket ds;

    Client(DatagramSocket ds) {
        this.ds = ds;
    }

    public void run() {
        /* 不断从键盘读取数据并发送  */
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        try {
            while((line = bufr.readLine()) != null) {
                if(line.equals("over"))
                    break;
                byte[] buf = line.getBytes();
                DatagramPacket dp = new DatagramPacket(
                        buf, buf.length, InetAddress.getByName("localhost"), 10002);
                ds.send(dp);
            }
        } catch(UnknownHostException e) {
            throw new RuntimeException("Unknown host...");
        } catch(IOException e) {
            throw new RuntimeException("Read failed...");
        } finally {
            try {
                if(bufr != null)
                    bufr.close();
            } catch(IOException e) {
                throw new RuntimeException("Bufr shutdown failed...");
            }
        }
    }
}

class Server implements Runnable {
    private DatagramSocket ds;

    Server(DatagramSocket ds) {
        this.ds = ds;
    }
    /* 不断接收数据并打印在控制台 */
    public void run() {
        byte[] buf = new byte[1024];
        DatagramPacket dp = new DatagramPacket(buf, buf.length);
        try {
            while(true) {
                ds.receive(dp);
                String ip = dp.getAddress().getHostAddress();
                String data = new String(dp.getData(), 0, dp.getLength());
                System.out.println(ip + ": " + data);
            }
        } catch(IOException e) {
            throw new RuntimeException("Receive failed...");
        }
    }
}

这是一个程序同时实现发送和接收的任务,一个线程负责发送,一个线程负责接收。

  • 客户端建立TCP Socket服务
    TCP协议通讯分为客户端和服务端。客户端对应Socket类,服务端对应ServerSocket类。创建客户端对象时,就可以指定连接主机的地址和端口,形成数据通路。

第一步:
创建Socket服务并指定主机和端口

/* 创建客户端的Socket服务,指定目标主机和端口 */
Socket s = new Socket("localhost", 10002);

注意:
通路一旦建立,就存在一个Socket流;该流中既有输入流,也有输出流,可以使用getInputStream()方法和getOutputStream()方法获取输入流和输出流

第二步:
获取输出流,发送数据

/* 调用getOutputStream()方法,获取输出流,发送数据 */
OutputStream out = s.getOutputStream();
out.write("TCP data coming...".getBytes());

第三步:
关闭资源

/* 关闭Socket资源 */
s.close();
  • 服务端建立Socket服务

服务端原理:

这里写图片描述

客户端和服务端建立数据通路后,服务端会拿到该客户端的对象,使用该对象的输入和输出流与该对象进行通信。这样可以避免通信干扰,保证数据能被正确的客户接收。

第一步:
建立服务端Socket服务

/* 通过ServerSocket建立服务端Socket服务,绑定端口 */
ServerSocket ss = new ServerSocket(10002);

第二步:
获取连接的客户端对象

/* 调用accept()阻塞式方法获取连接对象 */
Socket s = ss.accept();

第三步:
服务端使用相应的客户端输入流读取数据

/* 获取客户端输入流对象读取数据 */
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
String ip = s.getInetAddress().getHostAddress();
System.out.println(new String(buf, 0, len));
System.out.println(ip + "connected...");

第四步:
关闭资源

/* 关闭客户端服务,释放服务端资源 */
s.close();
/* 可选操作,如持续服务,无须关闭 */
ss.close();

完整示例代码:

package com.heisejiuhuche.socket;
/* 客户端 */
import java.io.OutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws Exception {
        Socket s = new Socket("localhost", 10003);
        OutputStream out = s.getOutputStream();
        out.write("TCP data coming...".getBytes());
        s.close();
    }
}

package com.heisejiuhuche.socket;
/* 服务端 */
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(10003);
        Socket s = ss.accept();
        InputStream in = s.getInputStream();
        byte[] buf = new byte[1024];
        int len = in.read(buf);
        String ip = s.getInetAddress().getHostAddress();
        System.out.println(ip + "connected... " + new String(buf, 0, len));
        s.close();
        ss.close();
    }
}

程序运行结果:127.0.0.1connected... TCP data coming...

  • 客户端服务端互访
    客户端

第一步:
建立Socket服务,指定主机和端口

Socket s = new Socket("localhost", 10004);

第二步:
获取输出流,发送数据

OutputStream out = s.getOutputStream();
out.write("服务端你好,这里是客户端~".getBytes[]);

第三步:
获取输入流,接收服务端反馈信息

InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));

第四步:
关闭客户端资源

s.close();

服务端

第一步:
建立Socket服务,监听端口

ServerSocket ss = new ServerSocket(10004);

第二步:
获取客户端输入流对象,读取数据

Socket s = ss.accpet();
/* 获取连接的主机IP地址并打印 */
System.out.println(s.getInetAddress().getHostAddress());
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));

第三步:
获取客户端输出流对象,发送数据

OutputStream out = s.getOutputStream();
out.write("哥们儿收到,你也好~".getBytes());

第四步:
关闭资源

s.close();
ss.close();

完整代码:

package com.heisejiuhuche.socket;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws Exception {
        Socket s = new Socket("localhost", 10003);
        OutputStream out = s.getOutputStream();
        out.write("哥们儿你好~".getBytes());

        InputStream in = s.getInputStream();
        byte[] buf = new byte[1024];
        int len = in.read(buf);
        System.out.println(new String(buf, 0, len));
        s.close();
    }
}

package com.heisejiuhuche.socket;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(10003);
        Socket s = ss.accept();
        System.out.println(s.getInetAddress().getHostAddress() + "connected... ");
        InputStream in = s.getInputStream();
        byte[] buf = new byte[1024];
        int len = in.read(buf);
        System.out.println(new String(buf, 0, len));

        OutputStream out = s.getOutputStream();
        out.write("哥们儿收到,你也好~".getBytes());
        s.close();
        ss.close();
    }
}

程序运行结果:

127.0.0.1connected... 
哥们儿你好~
哥们儿收到,你也好~

练习1:
要求从客户端写入文本数据,服务端接收到数据后转成大写发送回给客户端。

示例代码:

package com.heisejiuhuche.socket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
/* 客户端代码 */
public class TransTextClient {
    public static void main(String[] args) throws Exception {
        /* 创建Socket对象,指定主机和端口 */
        Socket s = new Socket("localhost", 10005);
        /* 建立键盘录入 */
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        /* 创建Socket输出流对象 */
        BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        /* 创建Socket输入流对象 */
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
        /* 往服务器端发送数据 */
        String line = null;
        while((line = bufr.readLine()) != null) {
            if(line.equals("over"))
                break;
            bufOut.write(line);

            String upper = bufIn.readLine();
            System.out.println("Server: " + upper);
        }
        /* 关闭资源  */
        bufr.close();
        s.close();
    }
}

package com.heisejiuhuche.socket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
/* 服务端代码 */
public class TransTextServer {
    public static void main(String[] args) throws Exception {
        /* 创建ServerSocket对象 */
        ServerSocket ss = new ServerSocket(10005);
        /* 拿到Socket对象 */
        Socket s = ss.accept();
        /* 打印客户端IP地址 */
        System.out.println(s.getInetAddress().getHostAddress());
        /* 创建Socket输入流,接收数据 */
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
        /* 创建Socket输出流,发送数据 */
        BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        /* 接收客户端数据,转换成大写再发送会客户端 */
        String line = null;
        while((line = bufIn.readLine()) != null) {
            bufOut.write(line.toUpperCase());
        }
        /* 关闭资源 */
        s.close();
        ss.close();
    }
}

问题:
服务端收不到信息,客户端无法继续输入。

原因:
-使用缓冲字符输出流,数据写入缓冲区,应该调用flush()方法刷新;
-客户端和服务端都有阻塞式方法,这些方法没有读到结束标记,就会一直等待,应该调用newLine()方法换行

修改两个while()循环:

/* 客户端while */
while((line = bufr.readLine()) != null) {
    if(line.equals("over"))
        break;
    bufOut.write(line);
    /* 加入换行,使readLine()方法能读到换行符并返回数据 */
    bufOut.newLine();
    /* 刷新缓冲区 */
    bufOut.flush();

    String upper = bufIn.readLine();
    System.out.println("Server: " + upper);
}
/* 服务器端while */
while((line = bufIn.readLine()) != null) {
    bufOut.write(line.toUpperCase());
    bufOut.newLine();
    bufOut.flush();
}

至此,程序运行正常。

使用PrintWriter,设置自动刷新缓冲区,只需在两个while循环中调用println()方法,既能刷新缓冲区,又能有换行符号。

简化代码如下:

PrintWriter pw = new PrintWriter(s.getOutputStream(), true);
pw.println(line);

注意:
-readLine()方法必须读到回车符才会返回数据;
-输入over的时候,服务端也停止运行;因为客户端的关闭资源的时候,会往Socket流中发送-1;而服务器端调用的readLine()方法,底层的read()读到了这个-1,也会返回一个-1;那么服务器端的循环就结束了

练习2:
上传文件,保存在服务器端。

示例代码:

package com.heisejiuhuche.socket;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class UploadFileClient {
    public static void main(String[] args) throws Exception {
        Socket s = new Socket("localhost", 10006);
        /* 创建file对象,关联要上传的文件 */
        File file = new File("C:\\Users\\jeremy\\Documents\\javaTmp\\Send.java");
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
        /* 创建缓冲字符输入流,用于读取文件 */
        BufferedReader bufr = new BufferedReader(new FileReader(file));
        PrintWriter pw = new PrintWriter(s.getOutputStream(), true);
        /* 现将要上传的文件名通知服务器端 */
        pw.println(file.getName());
        String line = null;
        while((line = bufr.readLine()) != null) {
            pw.println(line);
        }
        s.shutdownOutput();
        System.out.println(bufIn.readLine());
        /* 发送文件上传结束标记给服务器端,以便服务器端停止读取 */
        bufr.close();
        s.close();
    }
}

package com.heisejiuhuche.socket;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public interface UploadFileServer {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(10006);
        Socket s = ss.accept();
        System.out.println(s.getInetAddress().getHostAddress());
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
        /* 接收文件名 */
        File path = new File("C:\\Users\\jeremy\\Documents\\");
        File file = new File(path, bufIn.readLine());
        /* 如果文件不存在,创建文件 */
        if(!file.exists()) {
            file.createNewFile();
        }
        PrintWriter pw = new PrintWriter(new FileWriter(file), true);
        String line = null;
        while((line = bufIn.readLine()) != null) {
            pw.println(line);
        }
        PrintWriter pw2 = new PrintWriter(s.getOutputStream(), true);
        pw2.println("上传成功");
        pw.close();
        s.close();
        ss.close();
    }
}

程序运行结果:上传成功

注意:
上传的时候,首先最好把文件名传给服务器端,让服务器端建立文件来储存数据;如果重名,要进行重命名