客户端/服务器的简易实现

目录

一,网络编程套接字

二,UDP/TCP的区别(​编辑)

[三,UDP API使用](#三,UDP API使用)

[四,TCP API使用](#四,TCP API使用)


一,网络编程套接字

socket

socket(操作系统给应用程序的API,起了一个名字,就成为socket API)

socket API提供了两套API分别为UDP和TCP:

二,UDP/TCP的区别()

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

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

1), 此处谈到的链接,是抽象链接.通讯双方,如果保留了通信对端的信息(端口和IP),就相当于是"有链接",如果不保存对端的的信息,那就是"无连接"

2), 可靠传输/不可靠传输:"可靠"不是指100%能到达对方,而是"尽可能".网络环境非常复杂,但存在非常不确定的因素.相对来说,"不可靠"就是不考虑信息是否可以到达对方

TCP内置了一些机制,保障可靠传输

1)感知对方是否收到了

2)重传机制,在对方没收到的时候进行重传

UDP则无重传机制UDP完全不管发出去的数据是否传到对方

但是可靠传输,需要付出代价,设计就更加复杂,传输效率也会有些损失

3), TCP是面向字节流的传输过程就和文件流/水流类似

UDP是面向数据包的,这是传输数据的基本单位,一次发送/接收,必须发送/接收完整的UDP数据报

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

半双工:一个数据链路只能发送数据或者只能接受数据(单向通信)

三,UDP API使用

DatagramSocket:代表一个socket对象(这是操作系统的概念,socket就可以认为是是操作系统中广义的文件下里面一种文件类型,这样的文件就是网卡这种硬件设备抽象的的表达形式)所以他就和文件具有相同的性质,需要打开才可以读写,还需要及时关闭,会占用文件读取表里面的资源...

网卡有不同的型号,也会提供不同的API,就会使代码直接操控网卡不太好操作,把网卡封装成了"socket",使程序员不需要关注不同的硬件的差异和细节,统一的去操作socket对象,就能简介操控网卡

DatagramPacket:代表一个UDP数据报:

复制代码
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class UDPEchoClient {
    DatagramSocket socket=null;
    private  String serverIP;
    private int serverPort;
    public UDPEchoClient(String serverIP,int serverPort) throws SocketException {
        socket=new DatagramSocket();
        //不需要指定端口号,不指定,不代表没有,而是客户端这边的的端口号是系统自动分配的
        //原因,如果在客户端这里这边指定了端口号之后,由于客户端是在用户这边运行的,
        //并不清楚用户的电脑上有哪些程序都已经占用了那些端口号,如果你指定的端口号和电脑上运行
        // 其他程序的的端口号发生冲突,就会出现bug,让系统自动分配一个端口号,就可以确保分配到的是一个没有人使用的端口号
        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,构造出一个请求
            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("127.0.0.1",9090);
        client.start();
    }
}

复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UDPEchoServer {
    private DatagramSocket socket=null;
    //socket的生命周期应该是跟随整个进程,进程结束了,socket才需要关闭,进程结束就会自定进行关闭,所以不需要专门去写close
    public UDPEchoServer(int port) throws SocketException {
        //对于服务器来说需要在socket对象创建的时候,就指定一个端口号,作为构造方法的参数
        //后续服务器开始运行的时候操作系统就会把端口号和该进程关联起来
        socket=new DatagramSocket(port);
        //再调用这个构造方法的过程中,JVM就会调用系统的socket API完成"端口号-进程"之间的关联动作
        //也被称为"绑定端口号"
        //对于操作系统来说,一个端口号只能绑定一个进程,但是一个进程可以创建多个socket对象,绑定多个端口号
    }
    public void start() throws IOException {
        System.out.println("服务器启用");
        while (true){
            //通过死循环,不停地处理客户端的请求
            //1,读取客户端的请求并解析,receive从网卡上读取数据,如果网卡上有数据,就会立刻receive返回,
            // 获得收到的数据,但是如果网卡上没有数据,就会阻塞等待.receive也是一个输出型参数
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //2,上述收到的数据,是二进制的byte[]的形式,后续如果要进行打印之类的处理操作,需要转化成字符串才好处理
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());
            //3,根据请求计算响应,由于此处是回显服务器,响应就是请求
            String response=process(request);
            //4,把响应返回给客户端
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),
                    response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //5,打印日志
            System.out.printf("[%s:%d] req=%s,resp=%s\n",requestPacket.getAddress()
                    ,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();
    }

}

注意这种写法,这里是获取的字节数组的长度,而非字符串的长度,如果字符串里面都是英文字母/阿拉伯数字/英文标点符号的话,都是ASCII编码的,一个字符也是有一个字节那么长,但是如果字符串中有中文,是utf8编码的,一个中文是3个字节长度,utf8也是能兼容ASCII的,当使用utf8表示英文的时候和ASCII表示英文是完全相同的

此处通信是本机上的,如果客户端和服务器搞两个主机,如此能够跨主机通信吗?

答:如果把我们写的代码放到"云服务器上"此时就可以通信.如果把服务器代码运行到自己的电脑上,此时,其他人是无法访问到我这个服务器的,除非他和我连一样的WiFi(云服务器:也是一个电脑,但是硬件配件和性能相比于台式机要低很多,但是带有公网IP)

如何在跨主机通信??

1)先有一个云服务器

2)在本地电脑上有一个能连接服务器的"终端"软件

3)登录到服务器上

4)把写的服务器代码,打包为jar包文件,上传到服务器上(Java编译生成.class文件,把.class文件打成特定结构的压缩包就是.jar)

5)在云服务器上运行程序

6)运行客户端,连接服务器

通过端口后找到对应的PID,然后找到相对的应用程序:

同一个协议下,当接口号被两个进程同时使用的时候就会直接进行报错,这时候我们就需要决定,是给新的进程换一个端口号还是把旧的删掉

后面的数字是绑定端口9090进程的PID

四,TCP API使用

前面针对文件的操作,TCP socket来说,也同样适用

复制代码
public class TCPEchoClient {
    private Socket socket=null;

    public TCPEchoClient(String serverIP,int serverPort) throws IOException {
        socket=new Socket(serverIP,serverPort);
    }
    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 scannerIn=new Scanner(System.in);
            while (true){
                //1,从控制台读取数据
                System.out.print("->");
                String request=scannerIn.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 tcpEchoClient=new TCPEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }
}

----------------------------------------------------------------------------
复制代码
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){
            //TCP建立连接的过程是在操作系统内核完成的,accept操作,这时内核已经完成建立连接操作了,然后才可以"接通电话"
            //accept相当于是针对内核已经建立好的连接进行一个确认的作用
            Socket clientSocket=serverSocket.accept();
            //每一次服务器调用一次accept,就会创建一个新的socket对象,用来和客户进行"一对一服务
            Thread thread=new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.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 printWriter=new PrintWriter(outputStream);
            while (true) {
                //1,读取请求,并解析
                if (!scanner.hasNext()){
                    //如果scanner无法读取到数据,说明客户端关了,导致服务器这边读到了"末尾"
                    break;
                }
                String request=scanner.next();
                //2,根据请求计算响应
                String response=process(request);
                //3,把响应写回给客户端
                printWriter.println(response);
                printWriter.flush();
                //此处将\n作为请求和响应的结尾,因此后续客户端也会使用scanner.next来进行读取
                //outputStream.write(response.getBytes());
                //4,打印日志
                System.out.printf("[%s:%d] req=%s,resp=%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 {
        TCPEchoServer tcpEchoServer=new TCPEchoServer(9090);
        tcpEchoServer.start();
    }
}

三个bug:

1)客户发送数据后没有任何响应,此处的情况是,客户端并没有真正的把请求发送出去

原因:printWrite这样的类,以及其他的很多IO流中的类,都是"自带缓冲区"的,引入缓冲区后,进行写入数据的操作,不会立刻触发IO,而是先放到缓冲区中,等到缓冲区有一定量的数据后,再统一进行发送

解决:

可以引入flush操作,主动"刷新缓冲区"

2)当前的服务器代码,针对clientSocket没有进行close操作

ServerSocket和DatagramSocket他们的生命周期是跟随整个进程的,但是clientSocket是连接级别的数据,随着客户端断开连接,这个socket就不再使用了(即使同一个客户端,断开后,重新连接,也是一个新的socket和旧的socket不是同一个了),因此这里需要主动关闭掉(文件泄露)

3)尝试使用多个客户端同时连接服务器

同一时刻,只能给一个客户端提供服务,只有停止上一个客户端,才能服务下一个客户端(这是不科学的),当一个客户端连上服务器后,服务器代码就会进入processConnect内部的while循环,此时第二个客户端尝试连接的时候,无法执行第二个客户端发送的请求数据,都积压在操作系统内核的接收缓冲区中,第一个客户端退出后,processConnect中的循环结束了,于是外部的循环就可以执行了,于是就可以处理第二个客户端积压的数据了

解决:多线程

如果短时间内要创建大量的线程,那么当前代码就不够用了,需要引入线程池

短时间有大量客户端:

1)客户端发送一个请求就快速断开连接->线程池(线程池本质上服务与一个客户端,使用线程池就是在复用线程)

2)客户端持续的发送请求处理响应,连接会持续很久->这种情况依旧可以使用较少的线程,提供高效的服务,"IO多路复用"

网络开发的时候,使用更少的线程,处理更多的客户端,由于刚刚的线程只服务于一个客户端,这样的线程很容易发生阻塞,相比于解决请求的时间,更多的时间是出于阻塞的状态的.所以我们可以给一个线程分配更多的客户端进行处理(IO多路复用具体的方案有很多种,最知名的就是Linux下的epoll).epoll就是在内核中,搞了一个数据结构,你可以把多个socket放到这个数据结构里面,,同时刻,大部分的socket都是处于阻塞等待状态,少数收到数据的socket,epoll就会通过回调函数的方式,通知应用程序,应用程序就会调用少量的线程,针对这里的"有数据"的socket进行处理

一个进程中,有三个特殊的流对象(特殊的文件描述符)不需要关闭,当他们启动的时候,操作系统自动打开,他们的生命周期都是要跟随整个进程的(System.in=>标准输入;System.out=>标准输出;System.err=>标准错误)

长链接:客户端连上服务器之后,一个连接中,会发起多次请求,接受多次响应

短连接:客户端和服务器连接上以后,一个链接,只发一个请求,接受一个响应,然后就断开连接,因此可能会频繁地建立/断开链接,而建立断开连接也是有开销的

相关推荐
速盾cdn37 分钟前
速盾:CDN是否支持屏蔽IP?
网络·网络协议·tcp/ip
yaoxin52112338 分钟前
第二十七章 TCP 客户端 服务器通信 - 连接管理
服务器·网络·tcp/ip
内核程序员kevin40 分钟前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
PersistJiao2 小时前
Spark 分布式计算中网络传输和序列化的关系(一)
大数据·网络·spark
黑客Ash5 小时前
【D01】网络安全概论
网络·安全·web安全·php
->yjy5 小时前
计算机网络(第一章)
网络·计算机网络·php
摘星星ʕ•̫͡•ʔ6 小时前
计算机网络 第三章:数据链路层(关于争用期的超详细内容)
网络·计算机网络
.Ayang6 小时前
SSRF漏洞利用
网络·安全·web安全·网络安全·系统安全·网络攻击模型·安全架构