网络编程(TCP/UDP)

什么是网络编程

网络编程指的是,**网络上的主机,不同的进程,通过网络的方式实现通信。**同一个主机但是不同进程之间通信也是网络编程,但是我们主要编程对象是不同主机至今啊的通信。

网络编程实际上就是把传输层和应用层进行封装,然后利用java提供的api进行通过,代码的形式交给传输层然后进行通信。

网络编程的基本概念

客户端和服务器

  1. 客户端:发起通信的一方,实际上就是我们平时的应用之类的。
  2. 服务器:接受数据的一方,接收数据并进行处理。

客户端服务器的定义实际上是谁发起了通信,谁接受了数据。

请求和响应

**请求:**request,客户端给服务器发送的数据。

**响应:**response,服务器返回给客户端的数据。

Socket关键字

Socket是系统提供的方式用于网络通信,网络通信常常基于Socket关键字。

TCP和UDP是传输层的两个重要的协议。

TCP的特点

  • 有连接(必须要双方都接通了才能进行通信,需要三次握手四次挥手)。
  • 可靠传输(可以知道对方是否接收到了数据)。
  • 面向字节流(网络中的传输数据是字节模式,以字节为单位)。
  • 全双工(可以双向通信)。

UDP的特点

  • 无连接(就是类似于QQ直接发出去,无需等待对方建立连接)。
  • 不可靠传输(对方就算对方没有接收到,发送端也不知道有没有对方是否接收到)。
  • 面向数据报(单位是数据报)。
  • 全双工(可以双向通信)。

UDP编程:

1.DatagramSocket

Datagramsocket是UDP Socket的关键方法,用来发送和接受UDP数据。

构造方法:

重要方法:

2.DatagramPacket

DatagramPacket是UDP Socket发送数据的数据报(每次接收和发送数据的基本单位就是 数据报)。

构造方法:

3.UDP回显服务器

服务器和客户端都要指定一个端口号,但是一般服务器的端口号要显式指定,客户端不能显式指定,系统会自动分配。服务器需要把端口号明确下来,需要让别人找到。客户端的端口号不能指定,因为有可能被别人占用了(避免端口号冲突),交给系统分配。

服务器的端口号在程序员手里,服务器的哪些端口号被使用了,程序员都知道的。客户端在客户上面。一个服务器程序需要长时间运行。

new DatagramPacket()用来承载从网卡这边读到的数据,读到数据需要指定一个内存空间来保存这个数据。socket(网卡)读取数据,,并且保存到requestpacket里面。

receive会阻塞,直到客户端发送数据。

package network;

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

public class UdpEchoServer {
    //服务器代码,创建一个DatagramSocket,后续操作网卡

    private DatagramSocket socket=null;

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

        //socket=new DatagramSocket();这是让系统分配的方法。




    }
    public void start() throws IOException {
        while (true){
            //读取请求并且解析
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            //从网卡中读取数据,并且存储在packet中,有数据才会接受不然就会阻塞
            socket.receive(requestPacket);
            //拿到数据,并且放入到String中,取区间内的字节,构造成String,这里的getlength实际上不是4096,是收到的数据的真实长度
            String request=new String(requestPacket.getData(),0, requestPacket.getLength());
            //根据请求计算响应!!!!一般服务器最重要的
            String response=process(request);


            //将响应写回去
            //UDP是无连接的,每次都要指定数据要发给谁
            //构造数据报,需要指定数据内容,也要指定发给谁
            //不能直接getlength,获取字符为单位的如果都是英文单词,那字符字节一样,中午不一样,网络传输都是字节为单位
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);





            System.out.printf("[%s:%d] req=%s resp=%s",requestPacket.getAddress().toString(),responsePacket.getPort(),request,response);

        }



    }


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

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



    }
}

对于客户端,服务器的端口号可以由系统随机分配,但需要知道服务器的IP地址及端口号,不然就不知道发送数据给谁。

  1. 客户端发送数据

  2. 构造数据报通过socket发送给服务器

  3. 服务器进行读取并且返回给客户端

  4. 客户端输出发送的响应

    package network;

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

    public class UdpEchoClient {
    private DatagramSocket socket;
    private String address;
    private int port;

     public UdpEchoClient(String address, int port) throws SocketException {
         this.address = address;
         this.port = port;
         socket=new DatagramSocket();//这里表示服务器的随机端口创建
     }
     public void start() throws IOException {
         System.out.println("客户端启动");
         Scanner input=new Scanner(System.in);
         while (true){
             //没有输入数据的时候就跳出循环
             if(!input.hasNext()){
                 break;
             }
             //读取所有的数据
             String request=input.next();
             //将内容构造成datagrampacket发送出去,并且需要找到对方的ip地址和端口号
             DatagramPacket datagramPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(address),port);
             //通过网卡进行发送数据报
             socket.send(datagramPacket);
    
             //读取服务器内容并且显示在客户端。
             DatagramPacket responseDatagramPacket=new DatagramPacket(new byte[4096],4096);
             socket.receive(responseDatagramPacket);
             String response=new String(responseDatagramPacket.getData(),0,responseDatagramPacket.getLength());
             System.out.println(response);
    
    
    
         }
     }
    
     public static void main(String[] args) throws IOException {
         UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",9090);
         udpEchoClient.start();
     }
    

    }

但是实际上这个程序不能跨主机通信,如果想要实现跨主机通信,就要把程序部署到云服务器上面。

4.UDP翻译回显服务器

基于上述回显服务器,还可以实现出一些其他带有一点业务逻辑的服务器。

进行业务逻辑的修改实际上就是进行对回显服务器的继承,再实现更多的细节和代码。

上述的操作是在process的代码中实现的,我们只要进行继承然后重写方法就可以达到汉译英的效果了。

package network;

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

public class UdpDireServer extends UdpEchoServer{
    private Map<String,String> map;
    public UdpDireServer(int port) throws SocketException {
        super(port);
        map = new HashMap<>();
        map.put("cat", "小猫");
        map.put("bear", "小熊");
    }

    @Override
    public String process(String request) {
        if(map.get(request)!=null){
            return map.get(request);
        }
        return request;
    }

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

观察一下运行结果,发现没有问题。

以上就是UDP的回显服务器的开发和运行了。

TCP编程:

1.ServerSocket

ServerSocket是创建TCP服务端Socket的API(只能给服务器使用)。

构造方法:

重要方法:

2.Socket

Socket 类用于创建客户端 Socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket. (服务器端和客户端都能使用)

构造方法:

重要方法:

3.TCP回显服务器程序

TCP和UDP的区别就是TCP是有连接的,就和打电话一样需要一方接通另外一方才能进行通话。所以要等待客户端发起请求后,服务器确认接通之后,才可以进行通信,TCP的首要任务就是建立连接。

和UDP回显服务器一样,对于这里的服务器,同样需要指定端口号创建TCP服务器端Socket,即ServerSocket。

  1. 服务器启动之后,需要通过accept方法来监听当前端口。
  2. 成功建立连接之后,就可以返回一个Socket方法,这个对象保存了对端的信息,即客户端信息,可以用来接收和发送请求等(TCP是面向字节流),可以通过该方法来发送和接收数据。

后续流程和UCP回显服务器一致。此处由于每有一个客户端连接,就会有一个clientSocket,这里消耗的Socket会越来越多,因此每当一个客户端连接结束,就需要释放这个clientSocket。

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.time.temporal.IsoFields;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket;

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


    public void start() throws IOException {
        System.out.println("服务器启动");
        //ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true){
            while (true) {
                //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
                Socket clientSocket = serverSocket.accept();
                processConnection(clientSocket);
            }
            

        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        //返回ip地址和对应的端口
        System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
        try(OutputStream outputStream=clientSocket.getOutputStream();
            InputStream inputStream=clientSocket.getInputStream()) {
            //不断读取输入的数据
            while (true){
                Scanner scanner=new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n",
                            clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                //next是等到/n才结束,也就是用户输入换行(回车)
                String request=scanner.next();
                String response=process(request);

                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s:%d] request:%s response:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);

        }

    }finally {
            clientSocket.close();
        }
        }

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

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

    }
}

对于客户端,需要指定服务器的IP和端口号建立连接。使用 Socket(String host, int port) 创建Socket的时候,就开始发起与对应服务器建立连接的请求了。

实际上TCP回显服务器和UDP很相似。,但是TCP是面向字节流。

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

public class TcpEchoClient {
    private Socket clientSocket;

    public TcpEchoClient(String serverAddress, int serverPort) throws IOException {
        clientSocket=new Socket(InetAddress.getByName(serverAddress),serverPort);
    }
    public void start() {
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream();
            Scanner scanner=new Scanner(System.in)) {
            while (true){
                System.out.print("->");
                String request=scanner.next();

                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();

                //读取服务器发送的数据
                Scanner inputScanner=new Scanner(inputStream);
                String response=inputScanner.next();

                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
        tcpEchoClient.start();
    }


}

在这里我们需要先运行服务器,再运行客户端,通常服务器都需要先启动,不然客户端会因为连接不上服务器而报错。

实际上这里还存在一个问题。这里的服务器只能给先获取连接的客户端提供服务,如果其他客户端想访问则会失败。

分析过程:

  1. 第一个客户端连上服务器之后,服务器就会从accept这里返回(解除阻塞),然后进入到processConnection方法中。
    .接下来服务器就会在processConnection循环处理客户端的请求,只有当客户端退出之后,连接结束,才会退出循环。
  2. 而服务器在循环处理客户端请求的时候,第二个客户端发起连接请求,而服务器这里并不能执行到accept。因此并不能成功连接,只有当客户端退出,才会执行回到accept进行连接。

第二个客户端之前发的请求为什么能被立即处理?

  1. 当前TCP在内核中,每个 socket 都是有缓冲区的。客户端发送的数据通过客户端代码,已经写入到服务器的缓冲区了,这里数据确实发送了,只不过数据在服务器的接收缓冲区中。
  2. 一旦第一个客户端退出,回到第一层循环,执行accept连接操作,后续processConnection方法里的 next 就能把之前缓冲区的内容给读出来。

实际上可以通过多线程来解决此问题,为每个访问的客户端都创建一个线程,使其可以通过单独的线程来进行访问服务器。

4. 服务器引入多线程

    //多线程
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
            Socket clientSocket = serverSocket.accept();
 
            Thread t = new Thread(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }

但是实际上,像这样频繁创建和销毁线程对服务器来说是一个不小的开销。

  • 每有一个客户端连接,就会创建一个新的线程,每当这个客户端结束,就要销毁这个线程。
  • 如果客户端比较多,并且频繁连接、关闭,就会使服务器频繁创建和销毁线程

因此我们使用了线程池。

5.服务器引入线程池

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
 
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

6.TCP字典服务器

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class TcpDictServer extends TcpEchoServer{
    Map<String,String> map=new HashMap<>();


    public TcpDictServer(int port) throws IOException {
        super(port);
        map.put("cat","小猫");


    }

    @Override
    public String process(String request) {
        return map.getOrDefault(request,"未查找到单词");
    }

    public static void main(String[] args) throws IOException {
        TcpDictServer tcpDictServer=new TcpDictServer(9090);
        tcpDictServer.start();
    }
}
相关推荐
Yoyo25年秋招冲冲冲12 分钟前
Day60_补20250208_图论part5_并查集理论基础|寻找存在的路径
java·开发语言·数据结构·算法·leetcode·动态规划·图论
_平凡之路_12 分钟前
实现限制同一个账号最多只能在3个客户端(有电脑、手机等)登录(附关键源码)
android·java·vue.js·spring·servlet
suuijbd20 分钟前
浅谈自己对RPC的理解
网络·网络协议·rpc
magic 24524 分钟前
JUnit5 单元测试详解
java·开发语言·单元测试·junit 5
fenglei202034 分钟前
DevOps工具链概述
linux·服务器·网络·devops
zjkzjk771137 分钟前
dynamic_cast和static_cast和const_cast
linux·运维·服务器
rain雨雨编程1 小时前
黑马Redis详细笔记(实战篇---短信登录)
java·redis·缓存·框架·短信登录
众乐乐_20081 小时前
Spring实现AOP功能的原理:代理模式(JDK动态代理与CGLIB字节码生成技术代理)的理解
java·spring·代理模式
思茂信息1 小时前
CST的TLM算法仿真5G毫米波阵列天线及手机
网络·人工智能·5g·智能手机·软件工程·软件构建