网络编程(UDP\TCP回显服务器)

目录

套接字socket

TCP和UDP特点比较

特点

比较

UDP回显服务器/客户端的编写

[UDP的socket api](#UDP的socket api)

回显服务器

客户端

TCP回显服务器/客户端的编写

[TCP的socket api](#TCP的socket api)

回显服务器

客户端

优化服务器

1.关闭服务器创建的socket对象

2.引入线程池,为多个客户端提供服务


套接字socket

操作系统提供的网络编程的API称为"socket api",在传输层中,TCP和UDP两种协议的特点和差异非常大,操作系统中就提供了两套api来分别表示:

流式套接字 -> TCP使用

数据报套接字 -> UDP使用

除此之外操作系统还有其他的关于网络编程的API,比如Unix域套接字,只是本地主机上的进程和进程之间的通信方式,不能跨主机通信,现在很少使用。

TCP和UDP特点比较

特点

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

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

比较

有连接vs无连接:

有连接则是通信双方保存对方的信息,删除信息则断开连接,无连接则是通信双方不需要保存各自信息。在计算机中,各自保存双方的信息,就认为是建立了一个抽象的连接。

可靠传输vs不可靠传输:

可靠 != 安全,可靠值要传输的数尽可能的全部传输给对方,在网络通信过程中,可能会存在多种意外情况,比如丢包,丢包的过程是随机的,无法预知。为了对抗丢包,引入了可靠传输特点,TCP具体这一特点,内部提供了一系列机制来实现可靠传输,UDP则是不可靠传输,传输数据时不会关心数据是否到达,接收方是否收到。

面向字节流vs面向数据报:

TCP和文件操作都是面向字节流的,读写操作非常灵活。UDP面向数据报,传输的基本单位是一个个UDP数据报,每次读写只能读写一个完整的UDP数据报。

全双工vs版双工:

全双工:一条链路能够进行双向通信(TCP/UDP都是),在;一条链路上既可以接收也可以发送。

半双工:一条链路,只能进行单向通信,在Linux中,系统提供的一种软件资源:管道,就是半双工,接收和发送不能同时进行。

UDP回显服务器/客户端的编写

UDP的socket api

java对于系统提供的网络编程api(socket api)进行了进一步封装,进行UDP网络编程代码编写时,需要重点理解两个类:

(1)DatagramSocket:这个类是对操作系统socket概念的封装,系统中的socket可以理解为文件,socket文件可以视为网卡这个设备的抽象表示形式,针对socket文件的读写操作课相当于对网卡这个硬件设备进行读写。其实之前学习的*普通文件,就是对硬盘这个硬件设备的抽象,直接操作硬盘不方便,借助文件进行操作就可以很方便的完成。*类似于电视机的遥控器,通过遥控器来使用电视剧更加方便。

计算机中对具有"遥控属性"这样概念的叫做句柄(handle)。

(2)DatagramPacket:针对UDP数据报的抽象表示,一个DatagramPacket对象,就相当与一个UDP数据报,一次发送/一次接收就是传输了一个DatagramPacket对象。

回显服务器

Echo称为回显,正常服务器发送不同请求就会有不同响应,回显服务器就是请求发了什么,响应就是啥,这个过程不涉及计算和逻辑业务,是最简单的客户端服务器程序。

编写服务器程序时,首先需要确定端口号,客户端是主动的一方,服务器是被动的一方,客户端需要找到服务器在哪。

IP地址(服务器所在主机的IP地址),port端口号(一个主机上,有多个程序都要进行网络通信,需要把那个程序用的哪个端口号记录下来,并要确保一个端口号不能被两个或多个进程关联)。

java 复制代码
import java.io.IOException;
import java.net.DatagramSocket;

public class UdppEchoServer {
    private DatagramSocket socket = null;
    public UdppEchoServer(int port) throws IOException {
        socket = new DatagramSocket(port);
    }
}

可以看到抛出的一异常可以是IOException,也就是说明网络编程的本质是IO操作。

接下来要让服务器可以不停的处理请求,不停的返回响应:

第一步:读取请求并对请求进行解析,构造一个数据报类的实例,对客户端发送的数据进行接收,放入实例中,对数据进行解析,最后为了方便打印,将数据报中的二进制数拿出来,转换为String类型的数据,String有一个构造方法,通过字节数组来构造。

java 复制代码
 DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
 socket.receive(requestPacket);
 String  request = new String(requestPacket.getData(),0, requestPacket.getLength());

第二步:根据请求返回响应,由于是回显服务器,计算请求的方法直接返回就行,返回的响应使用String进行接收。

java 复制代码
String response = process(request);
public String process(String request) {
       return request;
}

第三步:将响应返回给客户端,发送时需要知道接收对象的IP和端口号,可以通过接收的UDP数据报拿到发送客户端的信息,拿到之后放到响应的数据报中使用send()方法发送,最后在服务器上面打印关键信息。

java 复制代码
//把响应写回到客户端
//构造UDP数据包
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,
                requestPacket.getSocketAddress());
//发送请求到客户端
 socket.send(responsePacket);
 System.out.printf("[%s %d] req = %s, resp =%s\n",requestPacket.getAddress(),requestPacket.getPort(),request,response);

让程序启动后不断运行,加上while循环,一个完整的UDP回显服务器就此完成了。

客户端

客户端需要有自己的端口号和ip地址,客户端和服务器在一台主机上时就可以写本地环回地址(127.0.0.1),端口号是服务器在创建socket时指定的端口号。

java 复制代码
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverPort;
    private String serverIP;
    public UdpEchoClient(String serverPort,String serverIP) throws SocketException {
        socket = new DatagramSocket();
        this.serverPort = serverPort;
        this.serverIP = serverIP;
    }
}

在创建socket对象时并没有指定端口号,这是因为操作系统会自动分配一个端口号,这个自动的端口号每次重启都会不一样。

服务器需要固定端口号,而客户端需要让系统自动分配:

(1)服务器要有固定端口号,是因为客户端需要主动给服务器发请求,如果服务器端口号不是固定的,客户端就会不知道把请求发给谁了。

(2)客户端需要系统自动分配,指定固定的端口号是不行的,指定客户端的端口号,可能会和客户端所在电脑上的其他程序冲突,一旦端口冲突,就会导致程序启动不了。服务器在自己手里,就算端口冲突也是可以调整的,但客户端不在本机上时,端口冲突难以解决。

客户端逻辑在编写时和服务器有相同之处,客户端发送请求到服务器后,使用UDP数据报来接受响应。

java 复制代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private int serverPort;
    private String serverIP;
    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverPort = serverPort;
        this.serverIP = serverIP;
    }
    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while (true) {
           //输入请求
            System.out.println("请输入请求: ");
            String request = scanner.next();
            //构造请求
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            //发送请求
            socket.send(requestPacket);
            //构造接收的数据报
            DatagramPacket responsePack = new DatagramPacket(new byte[4096],4096);
            //接收服务器返回的结果
            socket.receive(responsePack);
            //转换成String类型
            String response = new String(responsePack.getData(),0, responsePack.getLength());
            System.out.println(response);
        }
    }

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

服务器与客户端同时启动,客户端发送消息,服务器都会回显回来,并在控制台打印相应的ip地址,端口号,请求和响应:

TCP回显服务器/客户端的编写

TCP的socket api

主要涉及两个类:ServerSocket和Socket。

ServerSocket是专门提供给服务器使用的,ServerSocket在 实例化时调用的构造方法中自带端口号,实例化的对象包含一个accept方法,是一个类似接通功能的方法。

Socket是给客户端和服务器都提供服务,通过Socket的构造方法能够和指定的服务器建立连接。

TCP中通过使用InputStream和OutputStream进行文件读写操作,通过两个get方法来获取socket内部流对象。

java 复制代码
Socket socket = new Socket();
socket.getInputStream();
socket.getOutputStream();

Tcp是字节流传输,传输基本单位是字节。

ServerSocket和Socket的功能不同: 对于服务器来说,需要上来与客户端建立连接,建立连接要使用ServerSocket对象的accept方法,方法的返回值为socket类型;服务器一启动就会执行到建立连接的位置,如果此时没有客户端连接那么accept方法就会进入阻塞状态,直到有客户端连接。也就是说**,ServerSocket是用与建立连接使用的,连接后将socket对象交给socket进行处理。**

回显服务器

每次创建一个服务器对象都要创建一个SerrverSocket来连接客户端,创建服务器时要包含 端口号,否则客户端没有端口号就无法连接。

java 复制代码
public class TcpEchoServer {
    private ServerSocket serverSocket = null;

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

在服务器中创建start方法用来启动服务器,调用start方法后服务器开始不断的执行客户端的请求,不停的处理客户端的请求就需要不停的与客户端建立连接,通过调用process()方法去处理客户端的请求,将和客户端建立连接的信息使用socket接收,传递给process方法:

java 复制代码
public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            Socket socket = serverSocket.accept();
            possess(socket);
        }
    }

实现process方法,要注意TCP是面向字节流传输,此时进行读写操作时需要使用InputStream和OutputStream,这两个类在使用完后必须回收防止资源泄露,避免数据丢失,使用 try-catch方法里的try with source用法,把对象放到try()中,使用 完毕会自动回收资源。获得数据后可以使用Read类读取请求,但是Read类读取请求后得到的是byte数组,需要进一步转换成字符串 ,这里使用Scanner去读取可以直接转换成String类型:

java 复制代码
    private void possess(Socket clientSocket)  {
        System.out.println("客户端上线");
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            while (true) {
                String request = scanner.next();
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

使用Scanner类读取数据时,读到"空白符"(空白符是一类字符的统称,包括但不限于换行,回车,空格,制表符,翻页符等)才会读取完毕,客户端在发送数据时,务必在每个请求的末尾加上空白符。

由于TCP是按照字节来传输的,在实际传输中,应该使若干个字节构成一个"应用层的数据报",此时就可以通过使用空白符作为"分割符",来区分不同的数据报。

将读取的请求交给处理请求的函数,使用的是process函数,这里对请求的处理实际上是直接返回请求(回显服务器):

java 复制代码
String request = scanner.next();
String response = possessFun(request);
outputStream.write(response.getBytes());

在读取请求之前,应该判断文件是否有输入 ,可以在进入while循环之后使用if语句判断请求是否有输入,使用scanner的next方法来判断,请求到达后,并且带有明确的分隔符就会返回true,如果TCP断开连接,就会返回false,使用scanner读取到文件末尾或者TCP断开连接就会返回false,否则就会阻塞等待客户端继续发送请求:

Tcp断开连接->阻塞解除返回false。

Tcp没有断开连接->对方们没有发数据过来,阻塞等待。

客户端发送请求 -> 接触阻塞并返回true。

服务器完整代码:

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            Socket socket = serverSocket.accept();
            possess(socket);
        }
    }
    private void possess(Socket clientSocket)  {
        System.out.println("客户端上线");
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            while (true) {
                if(!scanner.hasNext()) {
                    System.out.println("客户端下线");
                    break;
                }
                String request = scanner.next();
                String response = possessFun(request);
                outputStream.write(response.getBytes());
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    private String possessFun(String request) {
        return request+" \n";
    }
}

客户端

客户端在构造方法中应该有服务器的ip地址和端口号,在构造过程中就和服务器建立连接。

java 复制代码
    private Socket socket = null;
    public TcpClientSocket(String address,int  port) throws IOException {
        socket = new Socket(address,port);
    }

建立start方法去启动客户端输入请求,同样使用try-catch语句实例化字节流对象,使用Scanner在控制台上输入请求,将接收的语句发送给服务器之前使用**'/n'**作为分割符 ,发送给服务器使用OutputStream,传输的单位是字节,将请求转换为byte进行发送。

java 复制代码
    public void start() {
        System.out.println("'客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            Scanner Rescanner = new Scanner(inputStream);
            while (true) {
                String request = scanner.next();
                request += "/n";
                outputStream.write(request.getBytes());
            }
        }

然后如同服务器一样,判断来自服务器的响应是否到达,使用scanner接收响应,在将接收的响应打印到控制台,客户端就完成编写:

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class TcpClientSocket {
    private Socket socket = null;
    public TcpClientSocket(String address,int  port) throws IOException {
        socket = new Socket(address,port);
    }
    public void start() {
        System.out.println("'客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            Scanner Rescanner = new Scanner(inputStream);
            while (true) {
                String request = scanner.next();
                request += "/n";
                outputStream.write(request.getBytes());
                if(!Rescanner.hasNext()) {
                    break;
                }
                String response = Rescanner.next();
                System.out.println(response);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

步骤:

1.客户端从控制台得到请求。2.将请求发送到服务器3.服务器接收请求4.服务器处理请求5服务器.将响应发送给客户端6.客户端接收响应7.将响应输出在控制台。

优化服务器

1.关闭服务器创建的socket对象

serverrSocket不需要特别关闭,因为生命周期是伴随整个服务器进程。客户端的socket也是如此,但是服务器用于接收客户端信息的socket就必须关闭,服务器会对应多个客户端,如果使用完毕后不关闭当前资源文件得不到释放,就会引起文件资源泄露。在服务器代码中加上finally语句释放socket对象。

2.引入线程池,为多个客户端提供服务

主线程处理accept,每次接收一个accept就创建一个线程来服务,这里使用可扩容的线程池:

java 复制代码
    public void start() throws IOException {
        System.out.println("服务器启动");
        //自动扩容线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            Socket socket = serverSocket.accept();
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        possess(socket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

文章到这里就结束了,感谢观看。

相关推荐
大丈夫立于天地间22 分钟前
ISIS基础知识
网络·网络协议·学习·智能路由器·信息与通信
hgdlip24 分钟前
IP属地与视频定位位置不一致:现象解析与影响探讨
服务器·网络·tcp/ip
doubt。29 分钟前
【BUUCTF】[RCTF2015]EasySQL1
网络·数据库·笔记·mysql·安全·web安全
sunnyday04263 小时前
feign调用跳过HTTPS的SSL证书校验配置详解
java·网络·https·ssl
Bug退退退1234 小时前
IP协议特性
服务器·网络·tcp/ip
zhao3266857514 小时前
东南亚静态住宅IP的优势与应用
网络·网络协议·tcp/ip
万亿少女的梦1685 小时前
WEB渗透技术研究与安全防御
开发语言·前端·网络·爬虫·安全·网络安全·php
╰つ゛木槿6 小时前
WebSocket实现私聊私信功能
网络·websocket·网络协议
Fly不安全6 小时前
HackTheBox靶机:Sightless;NodeJS模板注入漏洞,盲XSS跨站脚本攻击漏洞实战
网络·安全·web安全·web·xss·hackthebox·模板注入
泷羽Sec-pp6 小时前
XSS靶场通关详解
服务器·网络·xss