网络初识和网络编程(Java版)

前言

网络已经成为我们日常生活中不可分割的一部分,我们每天都会从网络上得到各种各样的信息,我们也会在网络上传播各种各样的信息,可以说我们使用的软件都是依赖于网络的。作为一个程序猿,在我们未来部署的软件中,客户也是通过网络来访问的,服务器的响应也是通过网络给到用户的。那么,了解一点关于网络的知识和简单的网络编程还是很有必要的。今天我们就来简单的认识一下网络和网络编程。

什么是网络

是一个将分散的、具有独立功能的计算机系统,通过通信设备线路连接起来,由功能完善的软件实现资源共享和信息传递的系统。简单来说就是利用“媒介”将“计算机”“连起来”实现“资源共享”。

显然在最最开始的时候,每个计算机都是独立的单元,,每台计算机都具备不一样的功能,但在某些业务场景下需要多台计算机协作完成某个复杂的大任务,无法互联互通是难以实现这样的需求。我们就设计出了“网络”来实现“互联互通”

网络的分类

网络按照覆盖范围可以分为个域网/局域网/城域网/广域网

我们在这里只介绍局域网和广域网。个域网是最近新兴起的概念,城域网与广域网相比也只是覆盖范围不同,这个概念就不在这里展开了

局域网

局域网,即 Local Area Network,简称LAN

可以理解为是在本地搭建的私网,不同的局域网之间完全独立,连接后才能相互通信

建立局域网的方式有很多,主要是依赖于网线/集线器/交换机/路由器这样的设备实现的

广域网

广域网即 Wide Area Network,简称WAN。通过路由器,将多个局域网连接起来,在物理上组成很大范围的网络,就形成了广域网。广域网内部的局域网都属于其子网

这些概念往往区分的并不严格,只需要在上下文环境中能理解含义即可

网络通信基础

前面我们介绍了网络的概念,最重要的是实现计算机之间的资源共享,实现这个功能的基础就是网络通信。下面我们就来简单的介绍一下网络通信

IP和端口

我们在实现网络通信遇到的第一个问题就是由A向B传输数据,如何精准定位到A和B?

概述

为了解决在网络上的精准定位的问题,我们引入了IP和端口,IP可以定位到网络上的唯一主机,端口可以定位到这台主机上的唯一的进程(进程是计算机资源分配的最小单位,我们后面会在文章中再介绍)

IP

我们常用的是IPv4,由32个二进制数字组成,通常用 . 号分割成每组八个共四组,为了进一步方便表示,也用点分十进制表示

几个特殊的ip
  • 127.0.0.1(某些场景下,域名 localhost 也会被DNS服务器翻译为这个ip):这是计算机的本机环回ip,可以理解为自己给自己发消息,常用于测试
  • 内网ip(10.0.0.0--10.255.255.255,172.16.0.0--172.31.255.255 ,192.168.0.0--192.168.255.255):这些ip是内网ip,可以理解为局域网ip,不被允许在互联网上标识主机。内网ip出现的原因是为了解决ipv4枯竭的问题,我们设计出NAT(地址转换技术),用一个IP标识局域网的位置,局域网内使用内网IP,内部的计算机通信可以直接使用内网IP,与局域网外的计算机通信时路由器或者其他在局域网与外网连接处的通信设备将内网IP转换为外网IP,允许不同的局域网有相同的内网IP地址,这极大程度的缓解了IPv4枯竭的问题,但同样治标不治本,根本的方法是IPv6,这不是今天的重点,不在这里展开。

端口

如果将我们的计算机比作一个驿站,IP只能准确的找到这个驿站,但东西给谁就是端口要解决的问题。在计算机这个驿站里一个很大的存储柜,它给每个柜子编上序号,0-65535(两个字节),每个进程绑定一个端口号,可以理解为每个人有一个专属的柜子,如果是个A的数据只需要放到A绑定的柜子里A就可以收到,这样就解决了给谁的问题。

两个不同的进程,不能绑定同一个端口号,但一个进程可以绑定多个端口号
一个柜子不能既放A的数据又放B的数据,但A可以有多个柜子

知名端口

系统端口号范围为 0 ~ 65535,其中:0 ~ 1023 为知名端口号,预留给重要的服务器程序使用,这些服务器一般提供应用层协议和服务

22端口:预留给SSH服务器绑定SSH协议
21端口:预留给FTP服务器绑定FTP协议
23端口:预留给Telnet服务器绑定Telnet协议
80端口:预留给HTTP服务器绑定HTTP协议
443端口:预留给HTTPS服务器绑定HTTPS协议

协议

前面我们已经认识到了数据是如何从A到B的,,但网络上传输的都是二进制数据,A如何包装数据,B拿到数据如何拆开,这就是协议要解决的问题。简单来说协议就是接收双方的约定,约定传输的数据以何种形式组织,数据又以何种形式解读

网络通信协议有三要素,分别是语法(数据与控制信息的结构或格式),语义(即需要发出何种控制信息,完成何种动作以及做出何种响应),时序(即事件实现顺序的详细说明)。笔者的理解,语法就是这句话要包含哪些东西,语义就是我说的东西你能听懂,时序就是这句话里的事啥时候做。

协议最终体现为在网络上传输的数据包的格式。协议的作用就是“书同文,车同轨”。不管你的计算机是哪个制造商的,不管你是哪个系统的,只需要符合这套协议我们都可以通信

五元组

在TCP/IP协议(这是我们实际使用中最常见的协议)中,用五元组来标识一个网络通信:
1. 源IP:标识源主机
2. 源端口号:标识源主机中该次通信发送数据的进程
3. 目的IP:标识目的主机
4. 目的端口号:标识目的主机中该次通信接收数据的进程
5. 协议号:标识发送进程和接收进程双方约定的数据格式
Windows下,可以使用netstat -ano查看五元组的信息,也可以通过netstat -ano | findstr 过滤字符串 得到想要的信息

协议分层

上面我们介绍到了TCP/IP协议,但这其实不是一个协议而是一组协议。TCP/IP将协议分为五层,还有一种OSI协议,它将协议分为七层。这两者的异同点我们会在后面展开,我们先来聊聊为啥要给协议分层

协议分层

往往实现网络通信需要规范的地方很多很多,产生了很多很多的协议,为了方便管理和维护,我们按功能为这些协议分层,每层协议只需要关注本层的实现,调用下层的接口为上层提供服务。这也增加了协议的扩展性,一层升级只需要升级本层内容,不改变接口特性,即可在上下层无感知的情况下实现了升级

两大模型

协议分层可以分为TCP/IP模型和OSI模型。

TCP/IP模型是我们日常使用中最广泛使用的模型,OSI模型是国际化组织定义的标准

OSI模型

OSI将把网络从逻辑上分为了7层,是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整。通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯

 这个模型我们不展开,它既复杂又不实用,我们主要介绍的TCP/IP协议

TCP/IP

TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求

  • 应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层。
  • 传输层:负责两台主机之间的数据传输。如传输控制协议 (TCP),能够确保数据可靠的从源主机发送到目标主机。
  • 网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。
  • 数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。有以太网、令牌环网,无线LAN等标准。交换机(Switch)工作在数据链路层。
  • 物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。

简单来说,物理层只关注网络通信的硬件设备,数据链路层关心一个节点到下一个节点的传输,网络层关系数据路由主要是传输路径的选择,传输层关心主机到主机,应用层关心应用到应用

对应关系

 常见网络设备分层

对于一台主机,它的操作系统内核实现了从传输层到物理层的内容,也即是TCP/IP五层模型的下
四层;
对于一台路由器,它实现了从网络层到物理层,也即是TCP/IP五层模型的下三层;
对于一台交换机,它实现了从数据链路层到物理层,也即是TCP/IP五层模型的下两层;
对于集线器,它只实现了物理层;
 

数据在网络中传播时,经过不同的网络节点,网络分层需要对应。简单来说就是经过类似于上述的网络设备时需要分用到该设备所处的层,再封装下去

封装分用

对于不同的协议层。对数据的叫法各不相同。在传输层叫做段(segment),在网络层叫做数据报
(datagram),在链路层叫做帧(frame)

应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)

首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息

数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中
的 "上层协议字段" 将数据交给对应的上层协议处理称为分用

封装

分用

网络编程

为什么要网络编程

为了共享网络上丰富的资源。所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的

什么是网络编程

网络编程,指网络上的不同主机,或者相同主机的不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。但我们主要处理的还是提供网络上不同主机,基于网络来传输数据资源

基本概念

接收端和发送端

发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

请求和响应

一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。

客户端和服务器

服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端
服务一般是客户端获取服务器的资源、服务器保存客户端的数据

常见的客户端服务器端模型
  • 客户端发送请求
  • 服务器接收请求
  • 服务器处理请求,得到响应
  • 服务器返回响应
  • 客户端展示响应数据

网络编程

我们前面着重介绍了TCP/IP协议,这是我们网络通信中使用最广泛的协议族,那么我们网络编程势必要和TCP/IP打交道,遵循它的协议。显然这时候如果我们想要实现网络编程就必须对各种协议有很深刻的认识,这大大提高了开发的难度,所以我们对协议族再进行高度封装,提供一组API,极大简化开发,这组API就是socket,翻译为套接字,直译为接口。Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

socket分类

Socket套接字主要针对传输层协议划分为如下三类

流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。
数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。
原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。

数据报套接字(基于UDP)

UDP的特点是无连接,不可靠传输,面向数据报,有接收缓存区无发送缓冲区,大小为64K

java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的UDP数据报
 

回显服务器

下面我们就基于数据报套接字实现一个回显服务器,首先我们先来了解一下DatagramSocket 类和DatagramPacket类

DatagramSocket 类

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报

构造方法

方法
void receive(DatagramPacket p)
接收UDP数据报,没有接收则会进入阻塞等待
void send(DatagramPacket p)
发送UDP数据报
void close()
关闭socket

DatagramPacket 类

构造方法
DatagramPacket(byte buf[], int length)
DatagramPacket(byte buf[], int offset, int length)
DatagramPacket(byte buf[], int length,SocketAddress address)
DatagramPacket(byte buf[], int offset, int length, SocketAddress address)

buf 保存接收的数据,offset从哪个位置开始(默认为0),length数据占多少字节,address为IP,port为端口

方法

 InetSocketAddress ( SocketAddress 的子类 )

当然前面的构造方法中的 SocketAddress 可以替换为InetSocketAddress+int(port)

服务器

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

/**
 * @Title: UDPServer
 * @Author zzzyh
 * @description: UDP服务器
 */
public class UDPServer {
    
    //定义一个UDP的socket,即DatagramSocket
    private DatagramSocket socket = null;

    //利用构造函数将socket初始化,并且绑定端口9090,因为是服务器端,我们很容易知道哪些端口未被使用,使用静态的端口更加容易维护
    public UDPServer() throws SocketException {
        this.socket=new DatagramSocket(9090);
    }

    public void stact() throws IOException {
        //提示信息
        System.out.println("服务器启动");
        //为创建数据报做准备
        byte[] rem = new byte[4096];
        //循环接收请求
        while (true){
            //创建数据报
            DatagramPacket packet = new DatagramPacket(rem, 0,rem.length);
            //读取请求到数据报中
            socket.receive(packet);
            //将请求转换为字符串
            String req = new String(packet.getData(),0,packet.getLength());
            //根据请求得到响应
            String resp = process(req);
            //将响应写入数据报中
            DatagramPacket ret = new DatagramPacket(resp.getBytes(), resp.getBytes().length, packet.getSocketAddress());
            //将响应返回给客户端
            socket.send(ret);
            //打印日志信息
            System.out.printf("[%s:%d] req=%s resp=%s\n",packet.getAddress(),packet.getPort(),req,resp);
        }
    }

    //服务器根据请求得到响应。这是个回显服务器,简单处理
    private String process(String req) {
        return req;
    }

    //main方法将服务器启动
    public static void main(String[] args) throws IOException {
        new UDPServer().stact();
    }
}

客户端

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

/**
 * @Title: UDPClient
 * @description: UDP客户端
 */
public class UDPClient {
    //定义一个UDP的socket,即DatagramSocket
    private DatagramSocket socket = null;
    //服务器端ip
    private String ip;
    //服务器端端口
    private int port;

    //初始化socket,服务器ip和端口
    public UDPClient(String ip, int port) throws SocketException {
        socket = new DatagramSocket();
        this.ip = ip;
        this.port = port;
    }

    public void stact() throws IOException {
        //提示信息
        System.out.println("客户端连接");
        //准备输入
        Scanner scanner = new Scanner(System.in);
        //循环发送请求
        while (true){
            //提示输入请求
            System.out.print("请输入:");
            //输入请求
            String req =scanner.nextLine();
            //创建发送给服务器的数据报
            DatagramPacket p = new DatagramPacket(req.getBytes(),req.getBytes().length, InetAddress.getByName(ip),port);
            //发送数据报
            socket.send(p);
            //准备构造接收响应的数据报
            byte[] rem = new byte[4096];
            //创建接收响应的数据报
            DatagramPacket ret = new DatagramPacket(rem,0,rem.length);
            //接收响应
            socket.receive(ret);
            //将响应转换为字符串
            String r = new String(ret.getData(),0,ret.getLength());
            //打印响应到控制台
            System.out.println(r);
        }
    }

    //启动客户端,并且绑定服务器
    public static void main(String[] args) throws IOException {
        new UDPClient("127.0.0.1",9090).stact();
    }
}

 流套接字(基于TCP)

tcp的特点是面向连接,可靠传输,面向字节,有接收缓冲区也有发送缓冲区。大小不限制

ServerSocket

ServerSocket 是创建TCP服务端Socket的API

构造

方法

 Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端
Socket不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据

构造

 方法

 服务器

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Title: TCPserver
 * @description: TCP服务器
 */
public class TCPserver {
    //创建服务器socket
    private ServerSocket serverSocket = null;
    //初始化服务器socket,并且绑定端口

    public TCPserver(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    private void stact() throws IOException {
        //提示信息
        System.out.println("服务器启动");
        //创建线程池,实现并发访问
        ExecutorService service = Executors.newCachedThreadPool();
        //循环获取请求并返回响应
        while (true){
            //获得与客户端的连接
            Socket socket = serverSocket.accept();
            //为每个请求创建独立的线程实现并发
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(socket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

    private void processConnection(Socket socket) throws IOException {
            //提供try获取输入输出流可以避免资源泄露的问题
            try (InputStream inputStream = socket.getInputStream();
                 OutputStream outputStream = socket.getOutputStream()) {
                //提示信息
                System.out.printf("[%s:%d] 客户端上线\n", socket.getInetAddress(), socket.getPort());
                //将输入流封装成Scanner
                Scanner input = new Scanner(inputStream);
                //循环获取请求
                while (true) {
                    //如果没有请求则返回客户端下线的提示信息
                    if (!input.hasNext()) {
                        System.out.printf("[%s:%d] 客户端下线\n", socket.getInetAddress(), socket.getPort());
                        break;
                    }
                    //得到请求
                    String req = input.next();
                    //根据请求计算响应
                    String resp = process(req);
                    //将输出流封装为PrintWriter
                    PrintWriter writer = new PrintWriter(outputStream);
                    //将响应返回
                    writer.println(resp);
                    //刷新缓冲区
                    writer.flush();
                    //打印提示信息
                    System.out.printf("[%s:%d] req=%s resp=%s\n", socket.getInetAddress(), socket.getPort(), req, resp);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }finally {
                //关闭资源
                socket.close();
            }
    }


    //服务器根据请求获取响应
    private String process(String req) {
        return req;
    }

    //绑定端口并启动
    public static void main(String[] args) throws IOException {
        new TCPserver(9090).stact();
    }
}

客户端

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * @Title: TCPclient
 * @Author zzzyh
 * @description: TCP客户端
 */
public class TCPclient {
    //创建socket对象
    private Socket socket = null;
    //初始化socket对象,绑定服务器的ip和端口
    public TCPclient(String ip, int port) throws IOException {
        socket = new Socket(ip,port);
    }
    private void stact(){
        //打印提示信息
        System.out.println("客户端启动");
        //标准输入流,用于接收用户请求
        Scanner scanner = new Scanner(System.in);
        //获取输入输出流并且封装成Scanner和PrintWriter,放在try里避免资源泄露
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream();
             PrintWriter writer = new PrintWriter(outputStream);
             Scanner input = new Scanner(inputStream)) {
            //循环发送请求
            while (true) {
                //提示输入请求
                System.out.print("请输入:");
                //用户输入请求
                String req = scanner.next();
                //将请求发送给服务器
                writer.println(req);
                //刷新缓冲区
                writer.flush();
                //得到服务器的响应
                String resp = input.next();
                //将响应输出到控制台
                System.out.println(resp);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            //关闭连接,避免资源泄露
            scanner.close();
        }
    }

    //绑定服务器IP和端口,并且启动客户端
    public static void main(String[] args) throws IOException {
        new TCPclient("127.0.0.1",9090).stact();
    }
}

结语

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值