网络编程概念详解模拟回显客户端服务器

目录

1.网络中重要的概念

1)IP地址:

2)端口号:

3)协议

协议分层

OSI七层模型(教科书)

TCP/IP五层模型

封装和分用

网络套接字

面试题:TCP/UDP的区别?

UDP数据报套接字编程

模拟一个回显客户端-服务器


1.网络中重要的概念

在网络通信中,如何区别是从哪台主机将数据传输到哪台主机上呢?这就需要IP地址来标识。

1)IP地址:

IP地址描述了一台主机在互联网上的位置,IP地址是使用一个32位的二进制数表示的,通常被分为4个“8位二进制数”也就是4个字节。

使用“点分十进制”这样的方式来表示“IP地址”。这样的方式更有利于人们的阅读。


2)端口号:

标识主机中发送数据,接收数据的进程,定位主机上的指定位置应用程序(进程)

一个主机上使用网络有的很多个程序,通过端口号区分,当前主机收到的数据是要交给哪些程序来使用。端口号同样也是一个整数,一个2个字节的整数。

虽然端口号是0->65535范围,但是0->1023这些端口都是有特定含义的,我们一般使用这个范围之外的。

问题:有了IP地址和端口号就可以定位网络中的唯一进程,但是网络通信是基于二进制0/1数据来传输的,如何告诉对方发送的数据是什么样呢?因为数据的形式多种多样,有文字有图片等等

3)协议

网络协议简称协议,所有网络数据传输经过所有的网络设备都必须遵守的规则,比如怎么样建立链接,怎么样相互识别。遵守统一的规则计算机才能相互通信交流

协议最终体现为网络传输数据包的格式。

为什么需要协议?

计算机传播的媒介是光信号和电信号,通过"频率","强弱"来表示0/1,相传数不同的信息就需要约定好双方的数据格式

  • 计算机的生厂商很多
  • 计算机操作系统很多
  • 计算机网络硬件设备也很多

如果让这些厂商生产的计算机顺利通信,就必须要有一个共同的标准来遵守


协议分层

了解协议之后,如何进行网络通信的问题就转化为如何设计网络通信协议

网络传输数据依靠光信号,电信号,电磁波,本质上是传输010101这样的一串二进制,通信时就需要约定好这串数字是什么意思..

由于网络通信十分复杂,仅仅一个协议约定网络通信中所有细节是不可能的.所以就会把一个大协议拆分许多小的协议,去解决某一类问题,再让这些小协议相互配合------协议分层

约定好,协议之间不能i相互调用,只能上层调用下层协议; 图中的最后就叫做 协议栈/协议族

协议分层的好处?

  • 降低使用的成本,使用某个协议的时候不需要关注其他协议的实现细节.
  • 降低整个协议的耦合性,灵活的变更某个层次的协议

OSI七层模型(教科书)

七层模型过于复杂不实用,所以只存在与教科书上,没有被应用.

TCP/IP五层模型

物理层考虑的较少,所以有时候就是四层模型

  • 应用层负责程序之间的沟通,程序具体要做什么事情.程序员网络编程主要负责这一层的处理
  • 传输层只关心网络通信中的起点和终点,不关心通信的中间细节
  • 网络层进行网络通信的中间路径规划,还需要负责地址的管理
  • 数据链路层针对上述规划的路径进行具体的实施
  • 物理层描述的是硬件设备需要描述的物理设备需要什么条件

设备驱动程序是操作系统和硬件通信,管理硬件使用的程序,驱动程序是厂商配套使用的.

主机实现了从传输层到物理层,也就是下四层模型

路由器实现了网络层到物理层下三层结构

交换机实现数据链路层到物理层的下两层结构

集线器只实现了物理层

这里说的交换机与路由器都是传统意义上的机器,现在随着技术发展已经可以实现3/4层的交换机,4层路由器.

所以真实情况下,交换机可能工作在网络层 甚至传输层,路由器也可能工作在数据链路层和网络层,甚至应用层.


封装和分用

封装就类似于字符串的拼接

在使用通信工具的时候,在输入框内一个内容发送; 这个内容就被读取构造成一个"应用层数据包"

应用层网络协议就描述了数据包的构造. 一般应用层的协议是程序员自己定义的.

例举一个定义应用层数据包的案例.

1.将内容打包成应用层数据包

数据包的格式:    发送者的账号_接受者的账号_发送时间_消息正文\n

数据包的样例:     1234567890_20520863360_2024-08-8-12:00:00_hellow\n;

进行网络传输数据 通常需要把一个结构化的数据转化成一个字符串;

把结构化的数据转化成二进制字符串 这个过程为序列化;

把二进制字符串转化为一个结构化的数据 这个过程为反序列化

2.通信应用程序会调用操作系统提供的API    (传输层给应用层提供的API)

操作系统会提供一个类似"发送数据"这样的API,然后应用程序就会把上述组织好的应用层数据包作为参数传进来,于是应用层数据包就到了系统内核里面,就进入到传输层的代码部分中去.

此时.传输层就会把上述应用层数据包封装成一个传输层数据包. 传输层有多种协议其中最主要的是TCP和UDP,假设此处使用的是UDP.看你代码中具体是哪种API就调用哪个协议.

计算机的封装就类似于字符串的拼接,

3.传输层构造好数据之后,就会继续调用网络层给传输层提供的API,把数据进一步交给网络层.

网络层里也有很多协议,最主要的是IPv4协议(简称`IP协议).  当然还有IPv6,但是当前仍是IPv4占主导地位,所以这里默认是IPv4.

IP协议就会把上述的 传输层数据包构造成 网络层数据包 

4.网络层继续调用API,把数据交给数据链路层处理

数据链路层常见的协议是以太网(平时插网线,进行上网的这种方式)

5.上述的数据进一步交给物理层(硬件设备)

网卡就会针对上述的二进制数据,进行真正的传输操作.就需要把数据0101这样的序列,转化为光信号/电信号/电磁波.


接收方视角,此处做的工作就是发送方的逆向工作,发送方的封装认为是快打包,接受就是快递拆解。

1.接收光电信号,把这样的信号还原成101001这样的二进制字符串

2.物理层转换回来的数据交给数据链路层,以太网拿到这个数据包,就会对其进行解析.

拿出这里的报头与载荷,根据报头中的一些信息,做出一些处理.  根据报头信息决定是丢弃还是转发,还是保留(向上解析)

3.网络层拿到上述解析好的数据,也要对其进一步解析,取出IP报头和荷载

拿出报头数据后也要进行解析,取出IP报头 与 载荷,决定是丢弃转发 或者是保留

4.传输层这边UDP协议,取出UDP报头 和 载荷

此处需要把载荷里的内容交给应用层协议,依赖UDP报头中的“端口号” ,端口号就是来区分不同的进程的。

5.数据就到了通信应用这样的程序里了

通信程序就要针对数据进行“反序列化”,针对这里的数据进行进一步的逻辑。

  • 需要把收到的消息显示到界面上
  • 播放消息铃声,提醒收到消息
  • 显示弹窗
  • 更新未读的消息列表

总结:什么是封装 与 分用

封装:是将数据从一个层级 包装 到另一个层级的过程。在网络传输中 数据从应用层到物理层会经历的多层包装。以上发送信息的举例就是封装。

分用:就是 封装的反过程,即从物理层到应用层 逐步剥离各个层级的包装。在接收端,数据最终被还原为原始的应用数据。


封装与分用的过程中可能存在很多交换机 路由器完成数据转发的过程。

中间过程的交换机 路由器也会涉及到 封装与分用。

交换机封装到 数据链路层决定数据是丢弃还是继续转发,不再继续分用(教科书上的交换机)

路由器封装分用到网络层,可以决定是数据继续转发 还是丢弃。


网络套接字

Socket套接字

是由系统提供的用于网络通信技术,基于TCP/UDP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。

本身为插槽的意思,是操作系统给应用程序传输层给应用层)提供的API。

这个API有两种,一种是TCP,一种是UDP

面试题:TCP/UDP的区别?

TCP有连接,可靠传输,面向字节流,全双工

UDP无连接,不可靠传输,面向数据报,全双工

解释:

有无连接:就是通信双方是否保留了通信对端的信息,保留就相当于是“有连接”,不保留就是“无连接”。

可靠不可靠传输:就是能不能将数据传输到对端,这里的可靠也不是%100,而是大概率。

TCP内置了一些机制能够保证可靠传输:

1.感知到对方是不是收到了

2.重传机制,在对方没有收到的时候进行重试。

UDP没有重传机制,也不管数据有没有顺利达到对端。

面向字节流:TCP传输的过程就和文件流/水流一样特点。

面向数据报:传输数据的基本单位不是字节而是 ”UDP数据报“一次发送或者接受的就是一个 UDP数据报。

全双工:一个通信链路,可以发送数据,也可以接收数据(双向通信)

半双工:一个通信链路,只能发送/只能接收(单向通信


UDP数据报套接字编程

DatagramSocket:代表一个Socket对象,通过代码直接操作网卡,不好操作,通过操作系统把网卡封装成Socket,应用程序不关心硬件差异 统一操作Socket对象,就能间接操作网卡。

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

DatagramSocket 构造⽅法:

⽅法签名⽅法说明
DatagramSocket()创建一个UDP数据报嵌套字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报嵌套字Socket,绑定到本机指定端口(一般用于服务器)

DatagramSocket ⽅法:

⽅法签名⽅法说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该⽅法会阻塞等待)
void send(DatagramPacket p)从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()关闭此数据报套接字

模拟一个UDP回显客户端-服务器

服务器代码:

package network;

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

public class UdpEchoServer {
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    // 通过 start 启动服务器的核心流程
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 此处通过 "死循环" 不停的处理客户端的请求.

            // 1. 读取客户端的请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            // 上述收到的数据, 是二进制 byte[] 的形式体现的. 后续代码如果要进行打印之类的处理操作
            // 需要转成字符串才好处理.
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求计算响应, 由于此处是回显服务器. 响应就是请求.
            String response = process(request);

            // 3. 把响应写回到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);

            // 4. 把日志打印一下.
            System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress(), requestPacket.getPort(),
                    request, response);
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

客户端代码:

package network;

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("启动客户端");
        Scanner scanner = new Scanner(System.in);

        while (true) {
            // 1. 从控制台读取到用户的输入.
            System.out.print("-> ");
            String request = scanner.next();
            // 2. 构造出一个 UDP 请求, 发送给服务器.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(this.serverIP), this.serverPort);
            socket.send(requestPacket);
            // 3. 从服务器读取到响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4. 把响应打印到控制台上.
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        // UdpEchoClient client = new UdpEchoClient("47.108.28.88", 9090);
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

TCP流套接字编程

和刚才UDP类似. 实现⼀个简单的英译汉的功能

API 介绍

ServerSocket

ServerSocket 是创建TCP服务端Socket的API。

ServerSocket 构造⽅法:

⽅法签名⽅法说明
ServerSocket(int port)创建⼀个服务端流套接字Socket,并绑定到指定端⼝

ServerSocket ⽅法:

方法签名方法说明
Socket accept()开始监听指定端口(创建时绑定对象端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端连接,否则阻塞等待
void close()关闭此套接字

Socket

Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服

务端Socket。

不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。

Socket 构造⽅法:

⽅法签名⽅法说明
Socket(String host, int port)创建⼀个客⼾端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接
Socket ⽅法:
⽅法签名⽅法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输⼊流
OutputStream getOutputStream()返回此套接字的输出流
模拟TCP回显客户端-服务器

服务器代码

import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description :
 * User : Lenovo
 * Date : 2024-08-10
 * Time : 9:44
 */
public class TcpEchoSever {
    private ServerSocket serverSocket = null;

    public TcpEchoSever(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动成功");
        while (true){
            Socket clientSocket = serverSocket.accept();
            //accept方法返回的是Socket类型的
            
            //这里创建多线程方便多个客户端同时连接服务器
            Thread t = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
    //针对每一个链接,进行处理
    private void processConnection(Socket clientSocket) throws IOException {
        //先打印客户端信息
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //获取Socket中的流对象。方便对请求的读取 和 发送
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){

            //使用Scanner包装一下inputStream,更方便地请求数据
            //从输入流里读取数据
            Scanner scanner = new Scanner(inputStream);
            //使用PrintWriter包装一下outputStream可以更好的写入数据
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //1.数据请求解析
                //scanner读取的数据如果为空就跳出
                if(!scanner.hasNext()){
                    //如果scanner无法读取数据,说明客户端关闭了连接,导致服务器这边读木末尾
                    break;
                }
                //通过Scanner从InputStream流里读取到请求
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);

                //3.把数据写回客户端
                //此处按照字节数组来写,也有别的方法
                //outputStream.write(response.getBytes());
                printWriter.println(response);//写入的自动末尾会加上\n
                printWriter.flush();//刷新缓冲区,否则多个客户端发送的请求都会卡在缓冲区里,只能执行一个客户端
                //4打印日志
                System.out.printf("[%s:%d] request=%s,response=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
            clientSocket.close();
        }
    }

    private String process(String request){
        return  request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoSever tcpEchoSever = new TcpEchoSever(9090);
        tcpEchoSever.start();
    }
}

客户端代码

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

/**
 * Created with IntelliJ IDEA.
 * Description :
 * User : Lenovo
 * Date : 2024-08-10
 * Time : 10:57
 */
public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String severIP,int severport) throws IOException {
        this.socket = new Socket(severIP,severport);
    }
    public void start(){
        System.out.println("客户端启动");

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){

            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            Scanner scannerrequest  = new Scanner(System.in);

                while (true){
                    System.out.println("请输入数据->");
                    String request = scannerrequest.next();

                    //2.把请求发送给服务器
                    printWriter.println(request);
                    printWriter.flush();
                    //3.读取响应
                    if (!scanner.hasNext()){
                        break;
                    }
                    String response = scanner.next();
                    //4.打印响应
                    System.out.println(response);
                }

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

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值