黑马程序员技术交流社区

标题: 【石家庄校区】就业班day11--网络编程 [打印本页]

作者: xiekai_sjz    时间: 2018-9-27 14:47
标题: 【石家庄校区】就业班day11--网络编程
day11 网络编程    今天我们来学习网络编程的相关知识,并使用网络编程的知识完成文件上传和模拟网站的两个案例。
    以下是今天的学习目标:


以下是今天的详细笔记:


网络编程基础软件结构介绍C/S结构:
        全称为Client/Server结构, 是指 客户端 和 服务器 结构
        常见程序有QQ, 迅雷等软件

B/S结构:
        全称为Browser/Server结构, 是指 浏览器 和 服务器 结构
        常见浏览器有IE, 谷歌, 火狐等

网络编程: 在一定的"协议"下, 实现两台计算机的通信的程序


网络通信协议
网络通信协议:
        通信协议是对计算机必须遵守的规则, 只有遵守这些规则, 计算机之间才能进行通信.
        协议中对数据的传输格式, 传输速率, 传输步骤等做了统一规定, 通信双方必须同时遵守, 最终完成数据交换

TCP/IP协议:
        Transmission Control Protocol/Internet Protocol, 传输控制协议/因特网互联协议.
        它定义了计算机如何连入因特网, 以及数据如何在它们之间传输的标准. 它的内部包含一系列的用于处理数据通信的协议, 并采用了4层的分层模型, 每一层都呼叫它的下一层所提供的协议来完成自己的需求

网络通信协议分类
UDP: User Datagram Protocol, 用户数据报协议
        特点:
                1. 无连接的不可靠协议
                2. 数据按包发送, 64K一个包
                3. 速度快效率高, 容易丢包
        用于视频直播, 网络电话

TCP: Transmission Control Protocol, 传输控制协议
        特点:
                1. 需要建立连接的可靠协议
                2. 数据传输无大小限制
                3. 速度慢效率低
        用于文件下载, 浏览网页
       
TCP通信的三次握手: TCP协议中, 在发送数据的准备阶段, 客户端与服务器之间的三次交互, 以保证连接的可

        1. 客户端向服务端发送验证信息, 等待服务器确认
        2. 服务端收到验证信息后, 回复客户端验证信息, 同时发送自己的一条验证信息
        3. 客户端收到服务端回复的信息, 确认自己之前发的信息无误, 并再次向服务器发回服务端的验证信息

网络编程三要素: IP地址
网络编程三要素:
        1. 通信协议
        2. IP地址
        3. 端口号
               
IP地址: 互联网协议地址(Internet Protocol Address). 是网络中计算机的唯一标识
        版本:
                IPv4: 192.168.1.2
                IPv6: ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
        特殊的IP地址: "127.0.0.1", "localhost", 都代表自己的电脑

常用DOS命令:
        // 查看自己电脑的IP地址
        ipconfig
        // 查看是否能连接到指定IP地址
        ping IP地址
        ping 192.168.31.2
网络三要素: 端口号
端口号: 计算机中进程的唯一标识
        端口号的取值范围: 0~65535的整数, 其中0~1024不建议使用
注意:
        通信的两端是2个计算机中的2个程序在相互通信, 所以2个程序都要有端口号. 端口号可以相同, 也可以不同, 相互之间能识别就行

补充:
域名: 便于记忆的主机名称, 可以解析到某个IP地址, 这样通过域名就能找到对应的IP地址

www.baidu.com -> 111.13.100.92

http:// www.baidu.com :80
------    -------------      ---
协议      IP地址               端口

TCP通信TCP通信程序概述(上)
java.net.ServerSocket类: TCP服务端
        // 构造方法
        ServerSocket(int port): 创建一个TCP服务端, 并监听指定端口
        // 成员方法
        Socket accept(): 监听数据, 会阻塞. 收到数据后返回Socket对象
        void close(): 关闭服务端ServerSocket

java.net.Socket类: TCP客户端
        // 构造方法
        Socket(String ip, int port): 创建TCP客户端对象
        // 成员方法
        OutputStream getOutputStream(): 获取输出流对象, 用于发送数据
        InputStream getInputStream(): 获取输入流, 用于接收数据
        void shutdownOutput(): 关闭输出流, 告知服务端数据发送完毕
        void close(): 关闭客户端Socket

TCP严格区分为 客户端(Client) 与 服务端(Server)

两端通信时步骤:
        1. 服务端程序需要先启动, 等待客户端的连接
        2. 客户端主动连接服务器端, 连接成功才能通信. 服务端不可以主动连接客户端

两端之间以 IO字节流 进行通信
一个服务端可以和多个客户端同时通信
TCP通信方式概述(下)
服务端(ServerSocket)可以通过 accept() 方法等待一个客户端(Socket)主动连接, 从而得到一个客户端对象(Socket), 来识别不同的客户端

服务端(ServerSocket)没有IO流, 是通过获取到每个客户端对象(Socket)的IO流来进行通信的.
        使用"客户端的InputStream"读取客户端发来的数据
        使用"客户端的OutputStream"向客户端回写数据

TCP程序: 客户端代码实现
java.net.Socket类: TCP客户端
        // 构造方法
        Socket(String ip, int port): 创建TCP客户端对象
        // 成员方法
        OutputStream getOutputStream(): 获取输出流对象, 用于发送数据
        InputStream getInputStream(): 获取输入流, 用于接收数据
        void shutdownOutput(): 关闭输出流, 告知服务端数据发送完毕
        void close(): 关闭客户端Socket
       
实现步骤:
        1.创建一个客户端对象 Socket, 构造方法绑定服务器的IP地址和端口号
        2.使用Socket对象中的方法 getOutputStream() 获取网络字节输出流OutputStream对象
        3.使用网络字节输出流OutputStream对象中的方法 write(), 给服务器发送数据
        4.使用Socket对象中的方法 getInputStream() 获取网络字节输入流InputStream对象
        5.使用网络字节输入流InputStream对象中的方法 read(), 读取服务器回写的数据
        6.释放资源(Socket)

注意:
        1.客户端和服务器端进行交互, 必须使用Socket中提供的网络流, 不能使用自己创建的流对象
        2.当我们创建客户端对象Socket的时候, 就会去请求服务器和服务器经过3次握手建立连接通路
                这时如果服务器没有启动, 那么就会抛出异常ConnectException: Connection refused: connect
                如果服务器已经启动, 那么就可以进行交互了
[Java] 纯文本查看 复制代码
// 创建客户端Socket对象, 指定要连接的服务端的IP和端口号
        Socket socket = new Socket("127.0.0.1", 8888);
        // 向服务端发送数据
        OutputStream os = socket.getOutputStream();
        os.write("你好服务器".getBytes());
        // 释放资源
        socket.close();

5分钟练习: 编写TCP客户端
需求:
编写TCP客户端, 连接服务端的IP是"127.0.0.1", 端口是8888, 发送"你好服务器"
代码:
[Java] 纯文本查看 复制代码
public class TCPClient {
    public static void main(String[] args) throws IOException {
        // 创建客户端Socket对象, 指定要连接的服务端的IP地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);

        // 通过客户端对象获取输出流对象
        OutputStream os = socket.getOutputStream();
        // 通过输出流对象写字节数据
        os.write("你好服务器".getBytes());

        // (一会儿实现)接收服务器数据


        // 释放资源, 关闭客户端
        socket.close();
    }
}

TCP程序: 服务器端代码实现
java.net.ServerSocket类: TCP服务端
        // 构造方法
        ServerSocket(int port): 创建一个TCP服务端, 并监听指定端口
        // 成员方法
        Socket accept(): 监听数据, 会阻塞. 收到数据后返回Socket对象
        void close(): 关闭服务端ServerSocket
       
服务器的实现步骤:
        1.创建服务器ServerSocket对象和系统要指定的端口号
        2.使用ServerSocket对象中的方法 accept(), 获取到请求的客户端对象Socket
        3.使用Socket对象中的方法 getInputStream() 获取网络字节输入流InputStream对象
        4.使用网络字节输入流InputStream对象中的方法 read(), 读取客户端发送的数据
        5.使用Socket对象中的方法 getOutputStream() 获取网络字节输出流OutputStream对象
        6.使用网络字节输出流OutputStream对象中的方法 write(), 给客户端回写数据
        7.释放资源(Socket,ServerSocket)
[Java] 纯文本查看 复制代码
// 创建服务端ServerSocket对象, 并指定自己监听的端口
        ServerSocket server = new ServerSocket(8888);
        // 调用accept方法等待客户端连接
        Socket socket = server.accept();

        // 通过客户端获取输入流, 读取客户端发来的数据
        InputStream is = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes, 0, len));

        // 通过客户端获取输出流, 向客户端回写数据
        OutputStream os = socket.getOutputStream();
        os.write("收到谢谢".getBytes());

        // 释放资源
        socket.close();
        // server.close();  // 客户端一般不关闭

5分钟练习: 编写TCP服务端, 补全TCP客户端接收代码
需求:
编写TCP服务端代码, 监听8888端口, 接收客户端发来的消息打印到控制台, 然后向客户端回写"收到谢谢"
补全TCP客户端接收数据的代码, 将服务器回写的数据打印到控制台
代码:
[Java] 纯文本查看 复制代码
public class TCPServer {
    public static void main(String[] args) throws IOException {
        // 创建服务端对象, 监听8888端口
        ServerSocket server = new ServerSocket(8888);

        // 等待客户端连接
        Socket socket = server.accept();

        // 当客户端连接上后, 先读取客户端发来的消息, 获取输入流
        InputStream in = socket.getInputStream();
        // 由于数据量较少, 只读一次, 不用循环
        byte[] bytes = new byte[1024];
        int len;
        len = in.read(bytes);
        // 转换为字符串打印
        System.out.println(new String(bytes, 0, len));

        // 向客户端回写数据
        OutputStream os = socket.getOutputStream();
        os.write("收到谢谢".getBytes());

        // 释放资源
        socket.close();
        server.close();
    }
}

public class TCPClient {
    public static void main(String[] args) throws IOException {
        // 创建客户端Socket对象, 指定要连接的服务端的IP地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);

        // 通过客户端对象获取输出流对象
        OutputStream os = socket.getOutputStream();
        // 通过输出流对象写字节数据
        os.write("你好服务器".getBytes());

        // 接收服务器数据
        InputStream in = socket.getInputStream();
        // 由于数据量较少, 只读一次, 不用循环
        byte[] bytes = new byte[1024];
        int len;
        len = in.read(bytes);
        // 转换为字符串打印
        System.out.println(new String(bytes, 0, len));

        // 释放资源, 关闭客户端
        socket.close();
    }
}

文件上传案例文件上传原理

文件上传案例: 客户端代码实现使用FileInputStream读图片
使用OutputStream发送图片到服务端
边读边发
5分钟练习: 实现客户端上传图片
需求:
编写客户端上传文件代码

实现步骤:
    1.创建一个本地字节输入流FileInputStream对象, 构造方法中绑定要读取的数据源
    2.创建一个客户端Socket对象, 构造方法中绑定服务器的IP地址和端口号
    3.使用Socket中的方法getOutputStream, 获取网络字节输出流OutputStream对象
    4.使用本地字节输入流FileInputStream对象中的方法read, 读取本地文件
    5.使用网络字节输出流OutputStream对象中的方法write, 把读取到的文件上传到服务器
    6.使用Socket中的方法getInputStream, 获取网络字节输入流InputStream对象
    7.使用网络字节输入流InputStream对象中的方法read读取服务回写的数据
    8.释放资源(FileInputStream, Socket)
代码:
[Java] 纯文本查看 复制代码
public class FileUploadClient {
    public static void main(String[] args) throws IOException {
        // 创建客户端Socket对象
        Socket socket = new Socket("127.0.0.1", 8888);

        /* 读取文件并发送给服务端 */
        // 创建文件字节输入流对象
        FileInputStream fis = new FileInputStream("d:\\小岳岳.jpg");
        // 获取网络字节输出流对象
        OutputStream os = socket.getOutputStream();
        // 一次一个字节数组, 循环读写
        byte[] bytes = new byte[1024];
        int len;
        while ((len = fis.read(bytes)) != -1) {
            // 读到的byte都通过网络输出流写出去
            os.write(bytes, 0, len);
        }

        /* 上传完毕后, 接收服务端返回的结果 */
        // 获取网络字节输入流对象
        InputStream is = socket.getInputStream();
        // 循环读取
        byte[] bytes2 = new byte[1024];
        int len2;
        while ((len2 = is.read(bytes2)) != -1) {
            // 将读取到的内容转换为字符串打印
            System.out.println(new String(bytes2, 0, len2));
        }

        // 释放资源
        fis.close();  // 自己创建的本地文件流要手动关闭
        socket.close();
    }
}

文件上传案例: 服务器端代码实现
使用InputStream从客户端接收数据
使用FileOutputStream写图片
边收边写
5分钟练习: 实现服务端接收图片
需求:
编写文件上传服务器代码, 将上传的文件保存到D盘upload目录中

实现步骤:
    1.创建一个服务器ServerSocket对象,和系统要指定的端口号
    2.使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
    3.使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
    4.判断d:\\upload文件夹是否存在,不存在则创建
    5.创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
    6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
    7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上
    8.使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
    9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功"
    10.释放资源(FileOutputStream,Socket,ServerSocket)
代码:
[Java] 纯文本查看 复制代码
public class FileUploadServer {
    public static void main(String[] args) throws IOException {
        // 创建服务端对象
        ServerSocket server = new ServerSocket(8888);
        System.out.println("图片上传服务器已启动!");  //打印一句话方便看服务端启动了
        // 等待客户端连接
        Socket socket = server.accept();

        // 创建上传图片所在的目录
        File uploadDir = new File("day11\\upload");
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }

        /* 读取客户端上传的数据, 写入到服务端磁盘 */
        // uploadDir + "\\小岳岳.jpg"  是对象与字符串拼接, 调用对象的toString()方法去拼接
        FileOutputStream fos = new FileOutputStream(uploadDir + "\\小岳岳.jpg");
        InputStream is = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len;
        while ((len = is.read(bytes)) != -1) {
            // 将网络流中读取到的数据写入到磁盘
            fos.write(bytes, 0, len);
        }

        /* 向客户端写回上传成功 */
        socket.getOutputStream().write("上传成功".getBytes());

        // 释放资源
        fos.close();
        socket.close();
        server.close();
    }
}

文件上传案例: 阻塞问题
java.net.Socket类: TCP客户端
        void shutdownOutput(): 关闭输出流, 告知服务端数据发送完毕
5分钟练习: 解决阻塞问题
需求:
修改客户端代码, 在发送完文件数据后, 增加一行代码发送结束标记: socket.shutdownOutput();
重新测试程序
代码:
[Java] 纯文本查看 复制代码
public class FileUploadClient {
    public static void main(String[] args) throws IOException {
        // 创建客户端Socket对象
        Socket socket = new Socket("127.0.0.1", 8888);

        /* 读取文件并发送给服务端 */
        // 创建文件字节输入流对象
        FileInputStream fis = new FileInputStream("d:\\小岳岳.jpg");
        // 获取网络字节输出流对象
        OutputStream os = socket.getOutputStream();
        // 一次一个字节数组, 循环读写
        byte[] bytes = new byte[1024];
        int len;
        while ((len = fis.read(bytes)) != -1) {
            // 读到的byte都通过网络输出流写出去
            os.write(bytes, 0, len);
        }
        // 关闭上传流
        socket.shutdownOutput();

        /* 上传完毕后, 接收服务端返回的结果 */
        // 获取网络字节输入流对象
        InputStream is = socket.getInputStream();
        // 循环读取
        byte[] bytes2 = new byte[1024];
        int len2;
        while ((len2 = is.read(bytes2)) != -1) {
            // 将读取到的内容转换为字符串打印
            System.out.println(new String(bytes2, 0, len2));
        }

        // 释放资源
        fis.close();  // 自己创建的本地文件流要手动关闭
        socket.close();
    }
}

文件上传案例: 优化(命名, 循环, 多线程)
解决方案:
        1. 上传文件的命名规则: 域名+毫秒值+随机数
        2. 将serverSocket.accept()直到最后的操作放入死循环中, 服务器就可以一直接收文件上传
        3. 当serverSocket.accept()得到Socket客户端对象后的操作, 全都放在子线程中执行
5分钟练习: 优化服务端代码
需求:
优化服务端代码, 解决文件重名问题, 无法一直接收问题, 上传效率问题
1. 拼接的文件名, 改为: 域名+毫秒值+随机数 的方式
2. 从serverSocket.accept()开始, 直到最后释放资源的操作代码, 全都放入while(true)死循环中
3. 将serverSocket.accept()之后, 直到最后释放资源的操作代码, 全都放到子线程中执行
代码:
[Java] 纯文本查看 复制代码
public class FileUploadServerBetter {
    public static void main(String[] args) throws IOException {
        // 创建服务端对象
        ServerSocket server = new ServerSocket(8888);
        System.out.println("图片上传服务器已启动!");  //打印一句话方便看服务端启动了

        // 优化2: 让服务端循环连接客户端
        while (true) {
            // 等待客户端连接
            Socket socket = server.accept();

            // 优化3: 当连接到一个客户端后, 将操作放在子线程中执行
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 创建上传图片所在的目录
                        File uploadDir = new File("day11\\upload");
                        if (!uploadDir.exists()) {
                            uploadDir.mkdirs();
                        }

                        // 优化1: 使用特殊的文件命名规则, 避免文件名重复
                        String filename = "itheima" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg";

                        /* 读取客户端上传的数据, 写入到服务端磁盘 */
                        // uploadDir + "\\小岳岳.jpg"  是对象与字符串拼接, 调用对象的toString()方法去拼接
                        FileOutputStream fos = new FileOutputStream(uploadDir + "\\" + filename);
                        InputStream is = socket.getInputStream();
                        byte[] bytes = new byte[1024];
                        int len;
                        while ((len = is.read(bytes)) != -1) {
                            // 将网络流中读取到的数据写入到磁盘
                            fos.write(bytes, 0, len);
                        }

                        /* 向客户端写回上传成功 */
                        socket.getOutputStream().write("上传成功".getBytes());

                        // 释放资源
                        fos.close();
                        socket.close();
                    } catch (IOException e) {
                        System.out.println(e);
                    }
                }
            }).start();
        }
//        server.close();  // 服务端永久运行, 不需要代码关闭
    }
}

模拟B/S案例模拟Web服务器: 分析

[Java] 纯文本查看 复制代码
InputStream is = socket.getInputStream()
InputStreamReader isr = new InputStreamReader(is)
BufferedReader br = new BufferedReader(isr);
//
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String s = br.readLine();

模拟Web服务器: 代码实现10分钟练习: 模拟Web服务器需求:
使用ServerSocket实现Web服务器

具体步骤:
        将web目录赋值粘贴到当前模块中
        创建ServerSocket对象, 监听8080端口
        使用while(true)死循环, 在循环中调用ServerSocket对象的accept()方法, 获取客户端Socket对象
        在循环中创建线程并start(), 在线程中传入Runnable的匿名内部类对象
        Runnable匿名内部类中编写读取请求返回页面的代码:
                1. 通过客户端socket获取InputStream, 经过转换流传递给BufferedReader, 便于读取一行字符串
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                2. 使用BufferedReader只读一行字符串即可, 然后按照空格切割字符串, 获取数组1索引的内容, 然后从1索引截取字符串到末尾
                        即: GET /day11/web/index.html HTTP/1.1 获取其中的 day11/web/index.html
                3. 创建FileInputStream指向截取出来的字符串作为文件路径
                4. 通过客户端socket对象获取OutputStream, 先写入以下内容:
                        OutputStream os = socket.getOutputStream();
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type: text/html\r\n".getBytes());
                        os.write("\r\n".getBytes());
                5. 然后使用一次读取一个byte[]数组方式, 通过刚才创建的FileInputStream对象和socket获取的OutputStream对象, 边读文件边写回客户端
                6. 关闭FileInputStream和socket, 释放资源
代码:
[Java] 纯文本查看 复制代码
public class WebServer {
    public static void main(String[] args) throws IOException {
        // 创建TCP服务端
        ServerSocket server = new ServerSocket(8080);
        // 打印一句话方便查看
        System.out.println("Web服务器启动了");

        // 循环接收客户端
        while (true) {
            // 等待客户端连接
            Socket socket = server.accept();

            // 在子线程中处理请求
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        /* 先读取浏览器发来的请求 */
                        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        // 读取一行字符串
                        String line = br.readLine();
                        // 按照空格拆分
                        String[] split = line.split(" ");
                        // 将中间的路径部分去掉开头的/
                        String htmlPath = split[1].substring(1);

                        /* 然后根据路径写回html文件 */
                        FileInputStream fis = new FileInputStream(htmlPath);
                        // 获取网络输出流
                        OutputStream os = socket.getOutputStream();
                        // 先写响应行和响应头
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type: text/html\r\n".getBytes());
                        os.write("\r\n".getBytes());
                        // 在写文件内容
                        byte[] bytes = new byte[1024];
                        int len;
                        while ((len = fis.read(bytes)) != -1) {
                            os.write(bytes, 0, len);
                        }

                        // 释放资源
                        fis.close();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

今日API
java.net.ServerSocket类: TCP服务端
        // 构造方法
        ServerSocket(int port): 创建一个TCP服务端, 并监听指定端口
        // 成员方法
        Socket accept(): 监听数据, 会阻塞. 收到数据后返回Socket对象
        void close(): 关闭服务端ServerSocket

java.net.Socket类: TCP客户端
        // 构造方法
        Socket(String ip, int port): 创建TCP客户端对象
        // 成员方法
        OutputStream getOutputStream(): 获取输出流对象, 用于发送数据
        InputStream getInputStream(): 获取输入流, 用于接收数据
        void shutdownOutput(): 关闭输出流, 告知服务端数据发送完毕
        void close(): 关闭客户端Socket



























作者: cuipu    时间: 2018-9-28 08:18
没想到啊,我居然还是沙发!
作者: 18032086639    时间: 2018-11-25 19:41
第二名
作者: xiekai_sjz    时间: 2018-11-25 21:24
18032086639 发表于 2018-11-25 19:41
第二名






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2