Java网络编程

网络发展 略

局域网/广域网

操作系统中的‘文件’是一个广义概念

平时说的文件,只是指普通文件

实际上,操作系统中的文件还可能表示了一些硬件设备/软件资源

socket 文件,就对应这‘网卡’这种硬件设备

从socket文件读数据,本质上就是读网卡

往socket文件写数据,本质上就是写网卡

最简单的客户端服务器程序,回显服务-EchoServer

这样的程序不涉及任何业务逻辑,就只是通过socket api 单纯的转发

socket = new DatagramSocket(port)

        此处在构造服务器这边的socket对象的时候,就需要显示的绑定一个端口号

端口号是用来区分一个应用程序的~主机收到网卡上数据的时候,这个数据该给哪个程序?

构造socket对象有很多失败的可能

1.端口号已经被占用了,两个人不能有相同的电话号码,同一个主机的两个进程也不能有相同的端口号

2.每个进程能偶打开的文件个数,是有上限的,如果进程之前已经打开了很多很多的文件,就可能导致此处的socket文件不能顺利打开。

第一步

package network;

import javax.sql.DataSource;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //进行网络编程,第一步就需要先准备好socket实例~这是进行网络编程的大前提
    private DatagramSocket socket = null;

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

第二步

启动服务器

//1.读取客户端发来的请求
//2.根据请求计算响应(由于咱们这是一个回显服务)
//3. 把响应写回到客户端

为啥服务器上来就是接受,而不是发送呢

因为服务器的定义:就是‘被动接受请求’的一方

主动发送请求的这一方,叫做客户端

send方法参数,也是DatagramPacket 需要把响应数据先构造成一个DatagramPacket 再进行发送

这里不是构造一个空的数据报

 这里的参数不再是一个空的字节数组了,response是刚才根据请求计算得到的响应,非空的

DatagramPacket里面的数据就是String response的数据

这里拿到的是字节数组的长度(字节的个数)

改进之后的版本在DatagramPacket 构造方法中,指定了第三个参数,表示要把数据发给哪个 地址 + 端口

socketaddres可视为一个类,里面包含了IP和端口

DatagramPacket reponsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

一  服务器完整代码

package network;

import javax.sql.DataSource;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

//   站在服务器的角度
//  1.源IP:服务器绑定的端口(此处手动指定 9090)
//  2.源端口:服务器绑定的端口(此处指定了 9090)
//  3.目的IP:包含在收到的数据报中(客户端IP)
//  4.目的端口:包含在收到的数据包中
//  5.协议类型UDP

public class UdpEchoServer {
    //进行网络编程,第一步就需要先准备好socket实例~这是进行网络编程的大前提
    private DatagramSocket socket = null;

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

    // 启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器");
        //UDP不需要建立连接,直接接受从客户端来的数据即可
        while ((true)){
            //1.读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);//为了接受数据,需要先准备好一个空的 DatagramPacket 对象,由receive 来进行填充数据
            // 把DatagramPacket 解析成一个String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //2.根据请求计算响应(由于咱们这是一个回显服务)
            String response = process(request);
            //3. 把响应写回到客户端
            DatagramPacket reponsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(reponsePacket);
            System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }
    //由于是回显服务,响应就和请求一样
    //实际上对于一个真实的服务器来说,这个过程是最复杂的,为了实现这个过程需要几万甚至几十万行代码
    private String process(String request) {
        return request;
    }

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

构造客户端

在客户端构造socket对象的时候,就不再手动指定端口号,使用无参版本的构造方法

不指定端口号,让操作系统自己分配一个空闲的端口号

通常手写代码的时候,服务器都是手动指定的,客户端都是由系统自动指定的

对于服务器来说,必须手动指定~后续客户端要根据这个端口来访问到服务器~

如果让系统随机分配,客户端就不知道服务器的端口是啥,不能访问。

对于客户端来说,如果手动指定,也行,但是系统随机分配更好

        一个及其上的两个进程不能绑定同一个端口

        客户端就是普通用户的电脑,程序复杂不,对应端口麻烦

        万一被占用,就无法正常工作了。

        而且由于客户端是主动发起请求的一方,客户端需要在发送请求之前先知道服务器的地址+端口

        但是反过来在请求出去之前,服务器是不需要事先知道客户端的地址+端口

 这种写法,也是,既构造了数据,有能构造目标地址,这个目标弟子,IP端口是合在一起的写法

写代码的时候,就会涉及到一系列的ip和端口

”五元组“

一次通信,是有五个核心信息,描述出来的

源IP,源端口,目的IP,目的端口,协议类型

完整代码

package network;

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

//  站在客户端的角度:
//  源 IP:本机IP
//  源端口:系统分配的端口
//  目的 IP:服务器的IP
//  目的端口:服务器的端口
//  协议类型UDP

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 星有野
 * Date: 2022-07-12
 * Time: 20:56
 */
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;



    public UdpEchoClient(String ip, int port) throws SocketException {
        //此处的 port 是服务器的端口
        //客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的
        socket = new DatagramSocket();
        serverIP = ip;
        serverPort = port;
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true){
            //1. 先把控制台读取用户输入的字符
            System.out.printf(" -> ");
            String request = scanner.next();
            //2. 把这个用户输入的内容,构造一个UDP请求,并发送
            //  构造的请求里包含两部分信息
            //  1)数据的内容 request 字符串
            //  2)数据要发给谁~ 服务器的 IP + 端口号
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                   InetAddress.getByName("127.0.0.1"),9090 );
            socket.send(requestPacket);
            //3. 从服务器读取响应数据报,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
            //4. 把响应结果显示到控制台上
            System.out.printf("req: %s, resp: %s\n", request, response);
        }

    }

    public static void main(String[] args) throws IOException {
        //由于服务器和客户端在同一个机器上,使用的IP仍然是127.0.0.1,如果是在不同的机器上,当然就需要改这里的IP
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();

    }

}

 

客户端可以有很多,一个服务器可以给很多很多客户端提供服务,一个餐馆可以给很多的客人提供就餐服务

能处理多少客户端取决于

1.处理一个请求消耗多少资源

2.机器一共有多少资源

当想再启动一个时会提示将之前的关掉

此时就需要进行一下操作

 写一个翻译程序

请求是一些简单的英文单词~响应也是英文单词对应的翻译

客户端不变,把服务器代码进行调整

主要是调整process方法

读取请求并解析,把响应写回给客户端,这俩步骤都一样

关键的逻辑就是  根据请求处理响应

private不可被重写需要改成public

public class UdpDictServer extends UdpEchoServer
package network;

import java.net.SocketException;


public class UdpDictServer extends UdpEchoServer{
    public UdpDictServer(int port) throws SocketException {
        super(port);
    }

    @Override
    public String process(String request) {
    }
}

把process重写即可

package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;


public class UdpDictServer extends UdpEchoServer{
    private HashMap<String, String> dict = new HashMap<>();
    public UdpDictServer(int port) throws SocketException {
        super(port);

        //提前存好
        //简单够早几个词
        dict.put("最美的人","皮皮");
        dict.put("最棒的摄影师","皮皮");
        dict.put("最可爱的人","皮皮");
        dict.put("最好的女朋友","皮皮");
        dict.put("皮皮的相机是","EOSRP");

    }
    //多态
    @Override
    public String process(String request) {
         return dict.getOrDefault(request,"该词无法被翻译");
    }

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

    }
}

 客户端不变,把服务器代码进行调整

主要是调整process方法

读取请求并解析,把响应写回给客户,这俩步骤都一样

关键的逻辑是”请求处理响应“

学习下TCP版本的客户端服务器的代码

TCP api中,也是涉及到两个核心的类

ServerSocket 专门给TCP服务器用的

Socket(既需要给服务器用,又需要给客户端用)

服务机完整代码

package network;


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;

public class TcpEchoServer {
    //  listen => 英文原意 监听
    //  但是再 Java socket 中是体现不出来 监听的含义的
    //  之所以这么叫,其实是 操作系统原生的API 里有一个操作叫做listen
    //  private ServerSockrt listenSocket = null;
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true){
            //由于TCP 是由连接的,不能一上来就读数据,而要先建立链接(接电话)
            // accept 就是在”接电话“,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
            // accept返回了一个socket对象,称为clientSocket,后续和客户之间的沟通,都是通过clientSocket来完成的
            //进一步讲 serverSocket 就干了一件事 接电话
            Socket clientSocket = serverSocket.accept();
            // 改进方法 在这个地方,每次accept 成功,都创建一个新的线程,由新线程负责执行这个 processConnection 方法
            Thread t = new Thread(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();

            processConnection(clientSocket);
        }

    }

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端按建立连接!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和响应
        //这里的针对 TCP socket 的读写就和文件读写是一摸一样的!!
        try(InputStream inputStream = clientSocket.getInputStream()){
            try(OutputStream outputStream = clientSocket.getOutputStream()){
            //循环的处理每个请求,分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true){
                    //读取请求
                    if(!scanner.hasNext()){
                        System.out.printf("[%s:%d] 客户端按建立连接!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //  此处用 scanner 更方便,如果不用Scanner就用原生的
                    String request = scanner.next();
                    //  2.根据请求,计算响应
                    String response = process(request);
                    //3.把这个响应返回给客户端
                    //为了方便期间,可以使用PrintWriter 把 OutputStream
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区
                    printWriter.flush();

                    System.out.printf("[%s:%d] 客户端按建立连接!",
                            clientSocket.getInetAddress().toString(),clientSocket.getPort());

                }

            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            //此处记得关闭操作
            clientSocket.close();
        }

    }

    private String process(String request) {

        return request;
    }

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

}

客户机完整代码

package network;


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

public class TcpEchoClient {
    // 用普通的 socket 即可,不用ServerSocket了
    // 此处也不用动手给客户端指定端口号,让系统自由分配
    private Socket socket = null;

    public TcpEchoClient(String serverIP,int serverPort) throws IOException{
        // 其实这里是可以给你的,但是这里给了以后,含义是不同的
        // 这里传入的 ip 和端口号 的含义表示的不是自己绑定,而是表示和这个ip端口建立连接
        // 调用这个构造方法,就会和服务器建立连接
        socket = new Socket(serverIP, serverPort);
    }

    public void start(){
        System.out.println("和服务器连接成功");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream()){
            try (OutputStream outputStream = socket.getOutputStream() ){
                while (true){
                    //要做的事情,仍然是四个步骤
                    //1.从控制台读取字符串
                    System.out.println("->");
                    String request = scanner.next();
                    //2.根据读取的字符串,构造请求,把请求发给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush();//如果不刷新,可能服务器无法及时看到
                    //3.从服务器读取响应,并解析
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    //4.把结果显示到控制台上
                    System.out.printf("req: %s, resp: %s\n",request,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();
    }
}

多线程

为啥UDP版本的程序没用多线程就正常

因为UDP不需要处理连接~UDP只要一个循环,就可以处理所有客户端的请求

但是此处TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会英雄到外层循环

主线程,循环使用accept,当客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干个请求,提供服务,在新线程里,通过while循环来处理请求,这个时候,多线程是并发执行的关系(宏观上看起来同时执行),就是各自执行各自的,就不会相互干扰

线程池也可以解决

前端后端通过网络来交互的

在这个交互的过程中,就需要约定好,前端发啥呀的数据,后端对应的数据

设计一个应用协议 

明确传输的信息

明确数据的组织格式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值