什么是网络编程

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

网络编程实际上就是把传输层和应用层进行封装,然后利用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();
        }
    }
    

    作者:看到我请叫我去刷leetcode

    物联沃分享整理
    物联沃-IOTWORD物联网 » 网络编程(TCP/UDP)

    发表回复