java网络编程

网络编程入门

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

软件结构

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。

在这里插入图片描述

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

在这里插入图片描述

网络通信协议

通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。

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

在这里插入图片描述

协议分类

tcp

TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前, 在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

tcp握手前提

传输层的作用就是建立端到端的连接。(例如客户端到服务端)

客户端和服务端是通过ip地址取得联系的,然而客户端和服务端都有各自的应用进程,并且都可能需要进行tcp连接。
例如在电脑中,同时用谷歌和火狐浏览器登录b站,b站需要把内容分发给两个不同的应用进程,这个时候就需要端口号(浏览器默认的端口号是443)来保证内容不会错发给应用进程。

因为走的是https协议,电脑会给谷歌和火狐分配不同的端口号(原因不清楚),这样进行连接就会像“管道”一样特定的进行传输。

ip地址加端口号就叫做套接字socket,套接字socket就是握手之前的核心条件。

在这里插入图片描述

tcp三次握手

TCP的三次握手和四次挥手实质就是TCP通信的连接和断开。

TCP协议位于传输层,作用是提供可靠的字节流服务,为了准确无误地将数据送达目的地,TCP协议采纳三次握手策略。

在使用tcp连接的时候就需要进行三次握手,但是怎么握手才能判断出哪些请求或者哪些响应需要丢弃,这才是握手机制的核心。

tcp报文里面有SYN,ACK,FIN等标识,如果设置1就是开启这些标识,设置0就是关闭这些标识。

  • SYN,synchronization(同步)
  • ACK,acknowledge(确认)
  • FIN,finsh(结束)
  1. 首先在客户端发送tcp报文的时候,会把SYN开启,客户端表示想和服务端进行数据的同步。

    仅仅把SYN开启是不够的,报文中还有一个重要的字段Sequence序号(随机生成),作为初始值来判断后续依据,更加保证了通道的唯一性。
    如果没有初始序号,如果同时发送两条tcp报文,那么接下来的握手不能保证这两条报文执行的顺序。

  2. 当服务器收到SYN以后,服务器会把SYN和ACK开启,就是确认同步的意思,服务器也会生成自己的Sequence序号和确认号(客户端的Sequence序号+1)

  3. 客户端确认,把ACK开启,此时的Sequence序号根据服务端的确认号-1生成,就知道是不是这条报文了,如果不确认的话,服务器还不知道自己发送出去的“确认同步”是否被接收。(确认号是服务端的确认号+1)

在这里插入图片描述

如果客户端每一次发送的SYN服务器都需要记住其序号,并生成自己的序号,那服务器就需要挂起非常多的资源,如果有黑客借此不断发送SYN又不进行下一步,就会导致服务器原地崩溃,也就是典型的DDos攻击。
因此服务器干脆不保存自己的序号,而是根据服务器的ip地址和端口号等私有信息进行算法的运算得到序号。

梳理总结
可以看出确认号都是根据对方的序号+1得到的,同时就控制位来说也是具有唯一性的,第一次是SYN,第二次是SYN+ACK,第三次是ACK。
两边不仅可以根据序号和确认号,还可以根据控制位来区分进行到哪个步骤,丢弃一些不必要的报文。

在同步(三次握手)以后,客户端就可以和服务端互相发送信息(数据传输),并保证数据传输的可靠性。
因为tcp是全双工的,所以可以互发信息。(看不懂)


第1次握手:客户端发送一个带有SYN(synchronize)标志的数据包给服务端,客户端表示想和服务端进行数据的同步;

第2次握手:服务端接收成功后,回传一个带有SYN/ACK标志的数据包传递确认信息,表示我收到了;

第3次握手:客户端再回传一个带有ACK标志的数据包,表示我知道了,握手结束。

其中:SYN标志位数置1,表示建立TCP连接;ACK标志表示验证字段。


三次握手的本质是确认通信双方收发数据的能力首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。这,就是三次握手。

tcp四次挥手

即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。

握手之后就建立了连接,客户端就可以发送http请求了,然后服务器响应内容,假设现在内容都交流完毕了,各自可能就会发起关闭连接的要求了。这个过程就是我们说的四次挥手。客户端和服务端都能主动发起关闭请求。

  1. 假设客户端主动发起关闭要求,客户端会在报文中开启FIN和ACK两个控制位(确认要结束会话)

  2. 服务端向客户端发送ACK确认,此时客户端并未正式关闭通道,因为服务端那边可能还有需要发送的数据。

  3. 服务端发送完数据以后再发送一个FIN+ACK来进行最后的确认。此时序号不需要改变,因为没有一来一回。只是多了一个控制位FIN来进行确认结束步骤而已。

  4. 客户端得到最终的结束确认以后会发送ACK来进行确认。


4次挥手
TCP 连接关闭时,需要进行四次通信来结束链接

客户端发送一个 FIN 请求,请求关闭
服务端接收FIN请求后,回复一个 ACK 请求,表示已经接收到了FIN
服务端也发送一个FIN请求,表示服务器已经没有数据需要发送了,请求关闭链接
客户端接收到服务端的FIN请求后,回复一个ACK,表示接收到服务端的 FIN,此时客户端进入 TIME_WAIT 状态,等待 2MSL 后,客户端关闭链接,服务端也进入关闭状态
需要四次挥手的原因是,TCP 连接是双向的,每一方都可以向另一方发送数据,因此在关闭连接时需要进行双向确认。

在 TCP 四次挥手中,客户端和服务器都需要发送FIN请求和ACK应答,以确保双方都已经关闭了连接。另外,客户端在发送完最后一个数据包后,可能会立即发送FIN请求,因此服务器需要先回复 ACK 应答,等待客户端发送FIN请求后再回复自己的 FIN 请求。这样,双方都可以确保数据传输的可靠性和完整性

为什么TCP连接的时候是3次?2次不可以吗?

两次不安全,四次没必要。

TCP 通信需要确保双方都具有数据收发的能力。

第一次带有 SYN 的 ACK 向客户端表明了服务端的收发能力,同时也验证了客户端自己的收发能力。
第二次的 ACK 则向服务端表明了客户端的收发能力(更准确来说是接收能力,因为第一条 SYN 就已经证明了客户端的发送能力),同时也验证了服务端自己的收发能力。


三次握手的主要目的是确认自己和对方的发送和接收都是正常的,从而保证了双方能够进行可靠通信。若采用两次握手,当第二次握手后就建立连接的话,此时客户端知道服务器能够正常接收到自己发送的数据,而服务器并不知道客户端是否能够收到自己发送的数据。

为什么TCP连接的时候是3次,关闭的时候却是4次?

TCP 握手时服务端将 SYN 和 ACK 合并在一个包中发送,因此减少了一次握手。

对于四次挥手,由于 TCP 是全双工通信,客户端(或者说主动关闭方)发送 FIN 请求只能表示客户端不再发送数据了,不代表完全断开连接,服务端(或者说被动关闭方)可能还要发送数据。所以不能将服务端的 FIN 包和对客户端的 ACK 包合并发送,只能先确认主动关闭方的 FIN,等服务端数据发送完毕时再发送 FIN 包,故挥手需要四次。


因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。

三次握手失败服务器会如何处理

握手失败有两种情况:

一种情况是服务端没有收到 SYN,此时不会做任何响应。
另一种情况是服务端在回复了 SYN + ACK 报文后长时间没有收到 ACK 响应,此时服务端会按照一定时间间隔重传一定次数的 SYN + ACK 报文,如果重传指定次数后仍未收到客户端的 ACK 响应,服务端会发送 RST 报文重置连接,释放资源。

udp

用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。

网络编程三要素

协议

计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。

IP地址

指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设 备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

IP地址分类

  1. IPv4:是一个32位的二进制数,通常被分为4个字节,表示成 a.b.c.d 的形式,例如 192.168.65.100 。其 中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。

  2. IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。 有资料显示,全球IPv4地址在2011年2月分配完毕。 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

端口号:用两个字节表示的整数,它的取值范围是0 ~ 65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。
如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

利用 协议 + IP地址(域名) + 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

http和tcp的区别

  • TCP 是一个传输层协议,而HTTP是一个应用层协议。HTTP通常基于TCP来传输数据。
  • TCP提供了可靠的、面向连接的数据传输,而HTTP建立在TCP之上,用于在客户端和服务器之间传输超文本文档和其他资源。
  • TCP是通用的传输协议,可以支持各种不同的应用,而HTTP是针对特定用途的协议,用于Web通信。
  • TCP确保数据的可靠性,而HTTP不负责数据传输的可靠性,它更关注应用层面的数据交换。

简而言之,TCP提供了底层的数据传输服务,而HTTP则建立在TCP之上,用于特定的应用场景,如Web浏览器与服务器之间的通信。

tcp通信程序

TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)

两端通信时步骤

  1. 服务端程序,需要事先启动,等待客户端的连接。

  2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

在Java中,提供了两个类用于实现TCP通信程序

  1. 客户端: java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。

  2. 服务端: java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。

Socket类

Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。

// :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址
public Socket(String host, int port) 


// 返回此套接字的输入流。 如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。 
// 关闭生成的InputStream也将关闭相关的Socket
InputStream getInputStream()

// 返回此套接字的输出流。 如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。 
// 关闭生成的OutputStream也将关闭相关的Socket
OutputStream getOutputStream()

// 关闭此套接字。 一旦一个socket被关闭,它不可再使用。 
// 关闭此socket也将关闭相关的InputStream和OutputStream 
void close()

// 禁用此套接字的输出流。 任何先前写出的数据将被发送,随后终止输出流
void shutdownOutput()

ServerSocket类

ServerSocket 类:这个类实现了服务器套接字,该对象等待通过网络的请求。

// 使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号
public ServerSocket(int port)

// 侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接
Socket accept()

简单示例

package com.example.demo;

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

public class ServerTcp {


    public static void main(String[] args) {
        try(ServerSocket serverSocket = new ServerSocket(8888)) {

            Socket socket = serverSocket.accept();

            System.out.println("socketServer 已启动");

            InputStream inputStream = socket.getInputStream();

            int len;

            byte[] array = new byte[1024];

            while ((len = inputStream.read(array)) != -1){
                System.out.println(new String(array,0,len));
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

package com.example.demo;

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

public class ClientTcp {

    public static void main(String[] args) {
        try(Socket socket = new Socket("127.0.0.1", 8888);) {
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("客户端发送成功".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件上传示例

package com.lxit.testnginx.testnginx;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;

public class TestSocketServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("文件上传服务器已正常运行");
        while (true) {
            Socket socket = serverSocket.accept();
            new Thread(() -> {

                // F:\testnginx
                String projectRootPath = System.getProperty("user.dir");

                // 给上传的文件取随机名
                String fileName = System.currentTimeMillis() + new Random().nextInt() + ".itcast";

                try (
                        InputStream socketInputStream = socket.getInputStream();
                        OutputStream socketOutputStream = socket.getOutputStream();
                        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(projectRootPath + "\\src\\main\\resources\\" + fileName)

                        )) {


                    System.out.println("文件正在上传");
                    Thread.sleep(10000);


                    byte[] bytes = new byte[1024];
                    int len;

                    while ((len = socketInputStream.read(bytes)) != -1) {

                        bufferedOutputStream.write(bytes, 0, len);

                    }

                    socketOutputStream.write(("文件上传成功").getBytes());
                    socket.close();

                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();


        }
    }
}

package com.lxit.testnginx.testnginx;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

public class TestSocket {

    public static void main(String[] args) {
        try (Socket socket = new Socket("127.0.0.1", 8888);
             OutputStream socketOutputStream = socket.getOutputStream();
             InputStream socketInputStream = socket.getInputStream();
             BufferedInputStream bufferedInputStream =
                     new BufferedInputStream(
                             new FileInputStream("F:\\javaee\\dailyQuest\\day12【网络编程】\\01_视频\\16_总结.itcast"))) {

            byte[] bytes = new byte[1024];
            int len;

            while ((len = bufferedInputStream.read(bytes)) != -1) {
                socketOutputStream.write(bytes, 0, len);
            }

            //文件发送完毕,给服务端一个结束标志,这样服务器也可以结束了
            socket.shutdownOutput();

            bytes = new byte[1024];
            len = 0;
            while ((len = socketInputStream.read(bytes)) != -1) {
                System.out.println(new String(bytes, 0, len));
            }


        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

模拟bs

package com.lxit.testnginx.testnginx;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TestSocketServer {

    public static void main(String[] args) throws IOException {

        // 创建服务器端serverSocket对象,绑定端口号
        ServerSocket serverSocket = new ServerSocket(8898);

        while (true) {
            // 获取客户端的socket对象
            Socket socketClient = serverSocket.accept();

            new Thread(() -> {

                try {


                    // 获取客户端请求的内容,socket对象获取字节输入流对象
                    InputStream socketClientInputStream = socketClient.getInputStream();

                    // 读取客户端发送过来的内容
                    //        int len;
                    //        byte[] bytes = new byte[1024*10];
                    //        len = socketClientInputStream.read(bytes);
                    //        System.out.println(new String(bytes,0,len));

                    // 创建高效字符输入流对象
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socketClientInputStream));

                    // 读取第一行内容,包含了浏览器端请求的路径
                    String responseText = bufferedReader.readLine();

                    // 获取请求中的文件路径
                    String path = "F:\\" + responseText.split(" ")[1].substring(1).replace("/", "\\");

                    // 浏览器要求在标签栏显示一个图标,而我们没有,会报错,则直接跳过
                    if (path.endsWith(".ico")) {
                        return;
                    }

                    // 创建文件字节输入流对象,读取本地文件,路径为path
                    FileInputStream fileInputStream = new FileInputStream(path);

                    // 获取socket对象的字节输出流对象
                    OutputStream socketClientOutputStream = socketClient.getOutputStream();

                    //给浏览器,响应数据之前,必须写出一下固定的3句话
                    socketClientOutputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
                    socketClientOutputStream.write("Content-Type:text/html\r\n".getBytes());
                    socketClientOutputStream.write("\r\n".getBytes());

                    int len;
                    byte[] bytes = new byte[1024 * 10];
                    while ((len = fileInputStream.read(bytes)) != -1) {
                        socketClientOutputStream.write(bytes, 0, len);
                    }

                    fileInputStream.close();
                    socketClientInputStream.close();
                    socketClientOutputStream.close();
                    socketClient.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();


        }


    }
}

部分知识引用自:
https://blog.csdn.net/xianlvfan2224/article/details/102722298
https://www.bilibili.com/video/BV18h41187Ep/?spm_id_from=333.337.search-card.all.click
https://blog.csdn.net/m0_49330686/article/details/129400464
https://blog.csdn.net/qq_43686863/article/details/129735764
https://blog.csdn.net/2301_76723322/article/details/137743450

我们每一个人在这个世界上都是孤独的。每个人都被囚禁在铁塔里面,只能与他的同伴通过信号交流,这些信号没有共同的所指,因此他们的含义是非常模糊和不确定的。
我们费尽力气想要把我们心中的珍藏传达给别人,可他们却没有领悟的能力,所以我们只能是形单影只、貌合神离,既不能了解别人,也不能为别人所了解。
我们像身处异国的他乡人,对那个国家的语言知之甚少,心中有许多美好和深刻的东西要倾诉,却只能局限于会话手册上的那几句陈腐的套语。
我们的头脑里充满了各种各样的思想,却只能说出像“园丁的姑妈有一把伞在屋子里”之类的话。

月亮与六便士
威廉·萨默赛特·毛姆

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值