网络编程详解
网络通信的要素
通信双方的地址
- IP地址
 - 端口号
 
通信协议
TCP/IP 参考模型
应用层
HTTP、FTP、SMTP
传输层
TCP、UDP
网络层
IP
数据链路层
Java 中的网络支持:
- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
 - URL:统一资源定位符;
 - Sockets:使用 TCP 协议实现网络通信;
 - Datagram:使用 UDP 协议实现网络通信。
 
IP
IP地址的类:InetAddress
IP地址能唯一定位一台网络上的计算机
127.0.0.1:本机 localhost
IP地址的分类
IPV4/IPV6
公网/局域网
192.168.xx.xx(专门给组织内部使用)
域名
InetAddress没有公有的构造函数,只能通过静态方法来创建实例:
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
//测试IP
public class TestIP {
    public static void main(String[] args) {
        try {
            //查询本机地址
            InetAddress inetAddress1 = InetAddress.getByName("localhost");
            System.out.println(inetAddress1);
            InetAddress inetAddress2 = InetAddress.getLocalHost();
            System.out.println(inetAddress2);
            //查询网站ip地址
            InetAddress inetAddress3 = InetAddress.getByName("www.baidu.com");
            System.out.println(inetAddress3);
            //常用方法
            System.out.println(inetAddress3.getCanonicalHostName()); //规范的主机名
            System.out.println(inetAddress3.getHostAddress()); // IP
            System.out.println(inetAddress3.getHostName()); // 域名或自己的计算机名
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}
端口
端口表示计算机上一个程序的对外的接口。
不同的进程有不同的端口号,用来区分软件
规定端口号范围:0-65535
相同协议下,端口号不能冲突(不同协议,可以使用相同端口号)
端口分类:
公有端口:0-1023
一般会被内置的进程、服务器使用,尽量不要使用
- HTTP端口: 80
 - HTTPS端口: 443
 - FTP端口: 21
 - SSH: 22
 - Telent: 23
 
程序注册端口:1024-49151
分配给用户或程序
- Tomcat: 8080
 - MySQL: 3306
 - Oracle: 1521
 - SQL Server:1433
 
动态、私有端口:49152-65535
netstat -ano #查看端口命令 netstat -ano|findstr "5900" #查看指定的端口 tasklist|findstr "1584" #查看指定端口的进程
public class TestSocketAddress {
    public static void main(String[] args) {
        InetSocketAddress socketAddress = new InetSocketAddress("localhost", 8080);
        System.out.println(socketAddress);
        System.out.println(socketAddress.getAddress());
        System.out.println(socketAddress.getHostName());
        System.out.println(socketAddress.getPort()); //获取端口
    }
}
URL
URL(统一资源定位符)用于定位网络资源,可以直接从URL中读取字节流数据。
public static void main(String[] args) throws IOException {
    URL url = new URL("http://www.baidu.com");
    /* 字节流 */
    InputStream is = url.openStream();
    BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf-8"));
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    br.close();
}
下载网络资源
URL url = new URL("https://pic3.zhimg.com/v2-77fd9b17781556ee25be582af6a9cd4c_r.jpg?source=1940ef5c"); // 资源地址(图片)
// 连接到该资源
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream is = urlConnection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is); // 图片、音频、视频等类型使用缓冲字节流,含中文的文档类资源使用缓冲字符流
// 判断文件夹是否存在
File file = new File("C:\\Users\\Hunter\\Downloads");
if (!file.exists()) {
    file.mkdirs();
}
FileOutputStream fos = new FileOutputStream(file + File.separator + "Hunter" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] bytes = new byte[4 * 1024]; // 存储每次读取的数据
int len = 0;
while ((len = bis.read(bytes)) != -1) {
    bos.write(bytes, 0, len);
}
bos.close();
bis.close();
urlConnection.disconnect();
通信协议
TCP/IP协议簇:
TCP(Transmission Control Protocol) 传输控制协议
UDP(User Datagram Protocol) 用户数据报协议
IP(Internet Protocol) 网络互连协议
TCP 和 UDP 的对比
TCP(类比:打电话)
需要连接,稳定
三次握手,四次挥手
// 至少需要三次,保证稳定连接 C:我喜欢你 S:我也喜欢你 C:我们在一起吧 // 四次挥手 C:我要走了 S:我知道你要走了 S:你已经走了吗? C:我已经走了有明确的客户端和服务端角色
传输完成后释放连接,效率低
UDP(类比:发短信)
- 不需要连接,不稳定
 
TCP
Socket:套接字,指两台设备之间通讯的端点,是包含了IP地址和端口号的网络单位。
java.net.ServerSocket服务器端类,相当于开启一个服务,并等待客户端的连接
java.net.Socket客户端类,向服务器发出连接请求,服务端响应请求后,两者建立连接,开始通信(三次握手)
服务端
java.net.ServerSocket
构造方法
ServerSocket(int port):创建绑定到指定端口的服务端套接字
成员方法
Socket accept():侦听并接收客户端的连接
实现步骤
- 创建服务器端对象ServerSocket,指定端口
 - 等待用户的连接,通过accept方法获取请求的客户端对象
 - 使用获取到的客户端对象的getInputStream方法获取网络字节输入流InputStream对象
 - 使用网络字节输入流对象的read方法读取客户端发来的数据
 - 使用获取到的客户端对象的getOutputStream获取网络字节输出流OutputStream对象
 - 使用网络字节输出流对象的write方法给客户端发送数据(传输结束标记)
 - 释放资源
 
服务器没有IO流,而是通过获取请求的客户端对象Socket,使用Socket提供的IO流和客户端进行交互:
- 服务器使用客户端的字节输入流InputStream读取客户端发送的数据
 - 服务器使用客户端的字节输出流OutputStream向客户端写回数据
 
ServerSocket serverSocket = new ServerSocket(2000);
while (true) { // 服务端的持久运行
    // 2. 获取请求的客户端对象
    Socket socket = serverSocket.accept();
    // 3. 读取客户端消息
    InputStream is = socket.getInputStream();
    BufferedReader br = new BufferedReader(new InputStreamReader(is));
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    // 向客户端发送数据
    OutputStream os = socket.getOutputStream();
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
    bw.write("你在教我做事?");
    bw.newLine();
    bw.flush(); // 重要
    // 网络流的传输不同于本地,不会传输结束的标记
    socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
    br.close();
    bw.close();
    socket.close();
}
客户端
java.net.Socket
构造方法
Socket(String host, int port):创建一个套接字,并连接到指定主机上指定的端口号。
- String host:服务器主机的名称/服务器的IP地址
 
成员方法
OutputStream getOutputStream():返回套接字的输出流InputStream getInputStream():返回套接字的输入流void close():关闭套接字
实现步骤
- 创建一个客户端对象Socket
 - 使用getOutputStream方法获取网络字节输出流OutputStream对象
 - 使用网络字节输出流对象的write方法给服务器发送数据(传输结束标记)
 - 使用getInputStream获取网络字节输入流InputStream对象
 - 使用网络字节输入流对象的read方法读取服务器传回的数据
 - 释放资源
 
Socket socket = new Socket("127.0.0.1", 2000);
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write("教练,我想打篮球");
bw.newLine();
bw.flush(); // 重要
// 网络流的传输不同于本地,不会传输结束的标记
socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
// 3. 读取服务端消息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
    System.out.println(line);
}
bw.close();
br.close();
socket.close();
文件上传
注意事项
- 为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如
WEB-INF目录下。 - 为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名(时间戳 + uuid + md5加密)。
 - 限制上传文件的最大值。
 - 限制上传文件的类型,判断后缀名是否合法。
 
服务端
- 创建服务器ServerSocket对象
 - 使用ServerSocket对象中的accept方法,获取到请求的客户端Socket对象
 - 使用Socket对象的getInputStream方法,获取到网络字节输入流
 - 判断指定存储的文件夹是否存在,不存在则创建
 - 创建一个本地字节输出流FileOutputStream对象
 - 读取客户端上传的文件,写入至服务器中指定的文件夹下
 - 获取网络字节输出流对象,返回上传成功的消息(传输结束标记)
 - 释放资源
 
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
    Socket socket = serverSocket.accept();
    InputStream is = socket.getInputStream();
    BufferedReader br = new BufferedReader(new InputStreamReader(is));
    // 判断文件夹是否存在
    File file = new File("C:\\Users\\Hunter\\Downloads");
    if (!file.exists()) {
        file.mkdirs();
    }
    // 使用【域名 + 系统时间 + 随机数】用于命名
    FileOutputStream fos = new FileOutputStream(file + File.separator + "Hunter" + System.currentTimeMills() + new Random().nextInt(999999) + ".md");
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
    // 将本地上传的文件内容输出至指定文件
    String line;
    while ((line = br.readLine()) != null) { // 客户端socket.shutdownOutput()的操作,会传来文件的结束标记
        bw.write(line);
        bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
    }
    bw.close();
    // 返回上传成功的消息
    OutputStream os = socket.getOutputStream();
    BufferedWriter bw2 = new BufferedWriter(new OutputStreamWriter(os));
    bw2.write("上传成功");
    bw2.flush();
    socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
    // 释放资源
    bw2.close();
    br.close();
    socket.close();
}
客户端
- 创建一个本地字节输入流FileInputStream对象,读取本地文件
 - 创建客户端Socket对象
 - 使用Socket对象的getOutputStream方法,获取到网络字节输出流
 - 通过网络字节输出流传输本地文件内容至服务器(传输结束标记)
 - 读取服务端返回的消息
 - 释放资源
 
// 创建本地字节输入流
FileInputStream fis = new FileInputStream("C:\\Users\\Hunter\\Downloads\\xx.md");
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
// 创建客户端Socket对象
Socket socket = new Socket("127.0.0.1", 8888);
// 获取网络字节输出流
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
// 将本地的文件上传至网络字节输出流
String line;
while ((line = br.readLine()) != null) {
    bw.write(line);
    bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
}
bw.flush();
socket.shutdownOutput(); // 上传完文件, 给服务端一个结束标记,否则服务端将阻塞
// 获取上传结果
InputStream is = socket.getInputStream();
BufferedReader br2 = new BufferedReader(new InputStreamReader(is));
while ((line = br2.readLine()) != null) {
    System.out.println(line);
}
// 释放资源
br.close();
br2.close();
socket.close();
文件上传优化 —— 多线程
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
    Socket socket = serverSocket.accept();
    /*
    * 使用多线程,提高程序效率
    * 有一个客户端上传文件,就开启一个线程,完成文件的上传
    * */
    new Thread(() -> {
        try {
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 判断文件加是否存在
            File file = new File("C:\\Users\\Hunter\\Downloads");
            if (!file.exists()) {
                file.mkdirs();
            }
            // 使用【域名 + 系统时间 + 随机数】用于命名
            FileOutputStream fos = new FileOutputStream(file + File.separator + "Hunter" + System.currentTimeMillis() + new Random().nextInt(999999) + ".md");
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
            // 将本地上传的文件内容输出至指定文件
            String line;
            while ((line = br.readLine()) != null) { // 客户端socket.shutdownOutput()的操作,会传来文件的结束标记
                bw.write(line);
                bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
            }
            bw.close();
            // 返回上传成功的消息
            OutputStream os = socket.getOutputStream();
            BufferedWriter bw2 = new BufferedWriter(new OutputStreamWriter(os));
            bw2.write("上传成功");
            bw2.flush();
            socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
            // 释放资源
            bw2.close();
            br.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }).start();
}
进阶
对于文件上传,浏览器将文件以流的形式提交到服务器,使用原生态的文件上传流request.getInputStream()获取十分麻烦,一般采用apache的开源工具commons-fileupload,其依赖于commons-io。
ServletFileUpload类
ServletFileUpload负责处理上传的文件数据,使用其parseRequest(HttpServletRequest)方法,会将表单中每个HTML标签封装成一个FileItem对象,以List的形式返回。
package com.hunter.servlet;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class FileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 判断上传的文件是否普通表单
        if (!ServletFileUpload.isMultipartContent(req)) {
            return;
        }
        // 创建上传文件的保存路径,建议在WEB-INF路径下,安全,用户无法直接访问上传的文件
        String uploadPath = this.getServletContext().getRealPath("/WEB-INF/upload");
        File uploadFile = new File(uploadPath);
        if (!uploadFile.exists()) {
            uploadFile.mkdir();
        }
        // 缓存临时文件
        //  临时路径,假如文件超过了预期的大小,就放到临时文件中,超过设定的时间后自动删除,或提醒用户转存为永久
        String tmpUploadPath = this.getServletContext().getRealPath("/WEB-INF/tmp");
        File tmpUploadFile = new File(tmpUploadPath);
        if (!tmpUploadFile.exists()) {
            tmpUploadFile.mkdir();
        }
        /*
         * ServletFileUpload负责处理上传的文件数据,使用其parseRequest(HttpServletRequest)方法,
         * 会将表单中每个HTML标签封装成一个FileItem对象,以List的形式返回。
         * 而使用ServletFileUpload对象解析请求时,需要DiskFileItemFactory对象。
         */
        try {
            // 1. 创建DiskFileItemFactory对象,处理文件上传 路径或大小限制
            DiskFileItemFactory diskFileItemFactory = getDiskFileItemFactory(tmpUploadFile);
            // 2. 获取ServletFileUpload
            ServletFileUpload servletFileUpload = getServletFileUpload(diskFileItemFactory);
            // 3. 处理上传的文件
            String msg = uploadParseRequest(req, uploadPath, servletFileUpload);
            req.setAttribute("msg", msg);
            req.getRequestDispatcher("info.jsp").forward(req, resp);
        } catch (FileUploadException e) {
            e.printStackTrace();
        }
    }
    private String uploadParseRequest(HttpServletRequest request, String uploadPath, ServletFileUpload servletFileUpload)
            throws FileUploadException, IOException {
        String msg = "";
        // parseRequest(HttpServletRequest)方法,会将表单中每个HTML标签封装成一个FileItem对象,以List的形式返回。
        List<FileItem> fileItems = servletFileUpload.parseRequest(request);
        for (FileItem fileItem : fileItems) {
            // 是否普通表单
            if (fileItem.isFormField()) {
                // 前端表单控件的name
                String fieldName = fileItem.getFieldName();
                // UTF-8 处理乱码
                String value = fileItem.getString("UTF-8");
                System.out.println(fieldName + ": " + value);
            } else {
                String uploadFileName = fileItem.getName();
                // 文件名可能不合法
                if (uploadFileName == null || uploadFileName.trim().equals("")) {
                    continue;
                }
                // 获得文件名
                String fileName = uploadFileName.substring(uploadFileName.lastIndexOf("/") + 1);
                System.out.println("文件信息【文件名:" + fileName + "】");
                // 使用UUID,保证文件名唯一。网络传输的东西,都需要序列化。
                String uuidPath = UUID.randomUUID().toString();
                String realPath = uploadPath + "/" + uuidPath;
                // 给每个文件创建一个对应的文件夹
                File realPathFile = new File(realPath);
                if (!realPathFile.exists()) {
                    realPathFile.mkdir();
                }
                // 获得文件上传的流
                InputStream inputStream = fileItem.getInputStream();
                // 创建一个文件输出流
                FileOutputStream fileOutputStream = new FileOutputStream(realPath + "/" + fileName);
                // 创建一个缓冲区
                byte[] buffer = new byte[1024 * 1024];
                int len = 0;
                while ((len = inputStream.read(buffer)) > 0) {
                    fileOutputStream.write(buffer, 0, len);
                }
                // 关闭流
                fileOutputStream.close();
                inputStream.close();
                msg = "文件上传成功";
                // 清除临时文件
                fileItem.delete();
            }
        }
        return msg;
    }
    /**
     * 获取ServletFileUpload
     *
     * @param diskFileItemFactory diskFileItemFactory
     * @return ServletFileUpload
     */
    private ServletFileUpload getServletFileUpload(DiskFileItemFactory diskFileItemFactory) {
        ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
        // 监听文件上传进度
        servletFileUpload.setProgressListener(
                (pBytesRead, pContentLength, pItems) ->
                        System.out.println("总大小:" + pContentLength + " B,已上传:" + pBytesRead + " B。"));
        // 处理乱码问题
        servletFileUpload.setHeaderEncoding("UTF-8");
        // 设置单个文件的最大值,10MB
        servletFileUpload.setFileCountMax(1024 * 1024 * 10);
        // 设置总共能上传文件的大小,10MB
        servletFileUpload.setSizeMax(1024 * 1024 * 10);
        return servletFileUpload;
    }
    /**
     * 创建DiskFileItemFactory对象,处理文件上传 路径或大小限制
     *
     * @param tmpUploadFile tmpUploadFile
     * @return DiskFileItemFactory
     */
    private DiskFileItemFactory getDiskFileItemFactory(File tmpUploadFile) {
        DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
        // 通过这个工厂类,设置一个大小为1MB的缓冲区,当上传文件大于这个缓冲区时,放到临时文件中
        diskFileItemFactory.setSizeThreshold(1024 * 1024);
        diskFileItemFactory.setRepository(tmpUploadFile);
        return diskFileItemFactory;
    }
}
UDP
java.net.DatagramPacket:数据报包,包括目的地址和传递的数据
java.net.DatagramSocket:数据包套接字,通过send方法发送数据报包DatagramPacket对象
发送端
// 1. 建立一个socket
DatagramSocket socket = new DatagramSocket();
// 2. 建一个包
String msg = "教练,我想打球";
InetAddress localhost = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.getBytes().length, localhost, 9090);
// 3. 发送包
socket.send(packet);
socket.close();
接收端
// 开放端口
DatagramSocket socket = new DatagramSocket(9090);
// 接收数据包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
// 3. 接收包
socket.receive(packet);
// 获取发送端的相关信息
System.out.println(packet.getAddress().getHostAddress());
System.out.println(new String(packet.getData(), 0, packet.getLength())); // 实际接收到的数据长度
// 4. 关闭连接
socket.close();
互相聊天
实现了Runnable接口的接收类和发送类:
public class TalkSend implements Runnable {
    DatagramSocket socket = null;
    BufferedReader reader = null;
    private int fromPort;
    private String toIp;
    private int toPort;
    public TalkSend(int fromPort, String toIp, int toPort) {
        this.fromPort = fromPort;
        this.toIp = toIp;
        this.toPort = toPort;
        try {
            // 1. 建立一个socket
            socket = new DatagramSocket(this.fromPort);
            // 控制台读取System.in
            reader = new BufferedReader(new InputStreamReader(System.in));
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        while (true) {
            try {
                // 2. 建一个包
                byte[] msg = reader.readLine().getBytes();
                DatagramPacket packet = new DatagramPacket(msg, 0, msg.length,
                        new InetSocketAddress(this.toIp, this.toPort));
                // 3. 发送包
                socket.send(packet);
                if (new String(msg).equals("bye")) {
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 4. 关闭连接
        socket.close();
    }
}
public class TalkReceiver implements Runnable {
    DatagramSocket socket = null;
    private int port;
    private String msgFrom;
    public TalkReceiver(int port, String msgFrom) {
        this.port = port;
        this.msgFrom = msgFrom;
        try {
            // 开放端口
            socket = new DatagramSocket(this.port);
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        while (true) {
            try {
                // 接收数据包
                byte[] buffer = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
                socket.receive(packet);
                // 判断是否断开连接
                String receiveData = new String(packet.getData(), 0, packet.getLength());
                System.out.println(msgFrom + ": " + receiveData);
                if (receiveData.equals("bye")) {
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
创建有发送和接收信息功能的对象
public class TalkTeacher {
    public static void main(String[] args) {
        // 发送进程一个端口,接收进程一个端口;发送的目的端口和接收的端口要与聊天的对象对应起来
        new Thread(new TalkSend(5555, "localhost", 8888)).start();
        new Thread(new TalkReceiver(9999,"student")).start();
    }
}
public class TalkStudent {
    public static void main(String[] args) {
        // 开启两个线程,发送进程一个端口,接收进程一个端口;发送的目的端口和接收的端口要与聊天的对象对应起来
        new Thread(new TalkSend(7777, "localhost", 9999)).start();
        new Thread(new TalkReceiver(8888, "teacher")).start();
    }
}
SMTP与POP3协议
SMTP
发送邮件。
POP3
接收邮件。
MIME
附件。
电子邮件
要在网络上实现邮件功能,必须有专门的邮件服务器。
SMTP服务器地址一般是smtp.xx.com。

要发送邮件,需要获得协议和支持,开启服务POP3/SMTP
例如网易邮箱:
