0
点赞
收藏
分享

微信扫一扫

【基于网络编程——全网最全最详细,没有之一~~~】

IT程序员 2022-04-13 阅读 72
java网络

一、基本概念:

1、首先我们明确两个概念:发送端和接收端

发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。

接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。

收发端:发送端和接收端两端,也简称为收发端。

※:发送端和接收端只是相对的,只是一次网络数据传输产生数据流后的概念。

2、请求和响应:

一般来说,获取一个网络资源,涉及到两次网络数据传输:

第一次:请求数据的发送。

第二次:响应数据的发送。

3、客户端和服务端

服务端:在常见的网络传输场景下,把提供服务的一方进程,称为服务端,可以对外提供服务。

客户端:获取服务的一方进程,称为客户端。

对于服务来说,一般是以下两种的提供方式:

①、客户端获取服务资源,即客户端发送请求到服务端,服务端根据客户端的请求进行响应计算,将计算结果返回到客户端。

②、客户端保存资源在服务端,通俗点讲,就是说客户端将数据发往服务端进行存储,服务端随之将存储结果返回(true/false)给客户端。

※:类比银行业务

·银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)

·银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管的现金)

二、Socket套接字

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程

分类:

主要使用的套接字为如下两种,其余的暂不讨论:

1、使用传输层TCP协议(流套接字):(类似于打电话,接通了才能进行传送信息)

特点:有连接、可靠传输、面向字节流、有接收缓冲区,也有发送缓冲区、大小不限

2、使用传输层UDP协议(数据报套接字):(类似于发微信,直接发送,不考虑对方是否收到)

特点:无连接、不可靠传输、面向数据报、有接收缓冲区,无发送缓冲区,大小受限:一次最多传输64k。

※:当数据的大小超过64k,推荐的两种做法是:一种是将数据进行划分,划分为多个小于64k的数据单元进行传输;另外一种是将UDP协议转化为TCP协议。

Socket编程注意事项:

 1、客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同的主机。

2、注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程。

3、Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议也需要考虑,后续我们说明如何设计应用层协议。

4、关于端口被占用的问题

如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。编程过程中我们会详细对这个问题进行处理,大致解决思路如下两种:

①.如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B

②.如果需要运行A进程,则可以修改进程B绑定的端口,换位其他没有使用的端口。

我们主要使用第二种解决方案来进行处理,一台电脑会有65536个端口,端口号从0开始到65535指定。当计算机运行时,我们并不清楚哪些端口被哪个进程所占用,此时我们需要通过操作系统来为我们分配空闲的端口供我们使用,以此方式避免端口被占用的问题。

三、UDP数据报套接字编程(DatagramSocket API)

DatagramSocket是UDP Socket,用于发送和接收UDP数据报

DatagramSocket构造方法:

 DatagramSocket方法:

 DatagramPacket是UDP Socket发送和接收的数据报

DatagramPacket构造方法:

 DatagramPacket方法:

 构造UDP发送的数据报时,需要传入SocketAddress,该对象可以使用InetSocketAddress来创建

InetSocketAddress API

 案例一(回显服务):

客户端向服务端发送什么,服务端就回应什么。

服务端代码:

public class UdpEchoServer {
    //进行网络编程,第一步就需要先准备好socket实例~这是进行网络编程的大前提.
    private DatagramSocket socket = null;
    //此处在构造服务器这边的socket对象的时候,就需要显式的绑定一个端口号,运行程序时指定即可
    public  UdpEchoServer(int port) throws SocketException {//构造socket对象有很多失败的可能
        socket = new DatagramSocket(port);
        //端口号是用来区分一个应用程序的~~主机收到网卡上的数据的时候,这个数据该给哪个程序?
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        //UDP不需要建立连接,直接接收从客户端来的数据即可~
        while (true){
            //1.读取客户端发来的请求
            //  1.1为了接收数据,需要先准备好一个空的DatagramPacket对象,由receive来进行填充数据
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//把字节数组包装了一下
            socket.receive(requestPacket);//receive对这个参数进行填充,“输出型参数”
            //把DatagramPacket解析成一个String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //2.根据请求计算响应(回显服务,2省略)
            String response = process(request);
            //3.把响应写回客户端
            //  3.1:send方法的参数,也是DatagramPacket,需要把响应数据先构造成一个DatagramPacket再进行发送
            //  3.2:这里就不是构造一个空的DatagramPacket了,而是有数据的
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),response.getBytes().length,requestPacket.getSocketAddress());
            System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
            //  3.3:在DatagramPacket构造方法中,指定了第三个参数,表示要把数据发送给哪个地址+端口
            socket.send(responsePacket);
        }
    }
    //由于是回显服务,响应和请求就是一样的了
    //实际上对于一个真实的服务器来说,这个过程是最复杂的,为了实现这个过程,可能需要n万行代码.....
    public String process(String request) {
        return request;
    }

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

 客户端代码:

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    //站在客户端的角度:
    //源IP:本机IP
    //源端口:系统随机分配的端口
    //目的IP:服务器的IP
    //目的端口:服务器的端口
    //协议类型:UDP
    public UdpEchoClient(String ip,int port) throws SocketException {
        //此处的port是服务器的端口
        //客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的~~
        socket = new DatagramSocket();
        serverIP = ip;
        serverPort = port;
    }
    public void start() throws IOException {
        while (true){
            //1.先从控制台读取用户输入的字符串
            Scanner scanner = new Scanner(System.in);
            System.out.print("-> ");
            String request =  scanner.next();
            //2.把这个用户输入的内容,构造成一个UDP请求,并发送
            //   构造的请求包含两部分内容
            //   1) 数据的内容,request字符串
            //   2) 数据要发给谁~服务器的IP+端口
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes(StandardCharsets.UTF_8).length,
                    InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            //3.从服务器读取响应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //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();
    }
}

案例二(字典服务-翻译服务):

客户端向服务器发送英文,服务端将接收到的英文对应的中文回应给客户端。

字典服务的底层逻辑和回显服务基本类似,重要的区别在于对请求的响应处理有所不同,实际开发过程中,对请求的响应处理也是最为复杂的~因此,此处省略客户端代码,客户端代码利用上述回显服务的客户端代码也可~

public class UdpDictServer extends UdpEchoServer{
    private HashMap<String,String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);
        //简单构造几个词
        dict.put("cat","猫咪");
        dict.put("dog","狗狗");
        dict.put("bird","小鸟");
        dict.put("frog","青蛙");
        dict.put("sun","太阳");
        dict.put("moon","月亮");
        dict.put("fuck","我草");
    }

    @Override
    public String process(String request) {
        String ret = null;
        if (dict.get(request) == null){
            ret = "该词无法查询~";
        }else {
            ret = dict.get(request);
        }
        return ret;
    }

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

 关于UDP数据报套接字编程的总结:

 1.服务器在构造时需要绑定端口号,而客户端则不需要

2.客户端在构造请求的时候,请求包含两部分内容,一个是用户输入的内容,另外一个是服务器的IP和端口号。

3.服务器和客户端在读取收到的数据时,都需要事先准备好一个空的DatagramPacket对象,通过receive来对这个对象进行填充,“输出型参数”

4.服务器在返回响应数据时,构造的就不是一个空的DatagramPacket对象了,而是有数据的,内容包含了数据内容、数据长度,以及requestPacket.getSocketAddress().

requestPacket.getSocketAddress().---->requestPacket是客户端发过来的数据, getSocketAddress:

 通过客户端发过来的数据获取客户端的IP+端口号,之后进行返回响应~

四、TCP流套接字编程(ServerSocket API)

ServerSocket API

serverSocket是创建TCP服务端Socket的API

ServerSocket 构造方法:

 ServerSocket方法:

Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

Socket构造方法:

 

Socket方法:

 扩展知识:TCP中的长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。

长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接,也就是说,长连接可以多次收发数据。

长短连接,区别如下:

1、建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。

2、主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。

3、两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

案例一(回显服务):

服务器代码(采用多线程,支持多个客户端同时访问服务器):

public class TcpThreadEchoServer {
    //listen => 监听
    //在Java socket中体现不出来“监听”的含义
    //之所以叫listen,是因为操作系统原生的API里有一个操作叫做listen
    //private ServerSocket listenSocket = null;
    private ServerSocket serverSocket = null;
    public TcpThreadEchoServer(int post) throws IOException {
        serverSocket = new ServerSocket(post);
    }
    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(() ->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和相应:
        //这里的针对TCP socket 的读写 和文件的读写是一模一样的~~
        try (InputStream inputStream = clientSocket.getInputStream()){
            try (OutputStream outputStream = clientSocket.getOutputStream()){
                //循环的处理每个请求,分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if(!scanner.hasNext()){
                        System.out.printf("[%s:%d] 客户端断开连接~\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用scanner更方便,如果不用scanner就用原生的InputStream的read也是可以的
                    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] req: %s,resp: %s\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //关闭操作
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
        tcpThreadEchoServer.start();
    }
}

客户端代码:

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 responseScanner = new Scanner(inputStream);
                    String response = responseScanner.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 tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }
}

案例二(字典服务--翻译):

public class TcpDictServer extends TcpThreadEchoServer{
    private HashMap<String,String> dict = new HashMap<>();
    public TcpDictServer(int post) throws IOException {
        super(post);
        //简单构造几个词
        dict.put("cat","猫咪");
        dict.put("dog","狗狗");
        dict.put("bird","小鸟");
        dict.put("frog","青蛙");
        dict.put("sun","太阳");
        dict.put("moon","月亮");
        dict.put("fuck","我草");
    }

    @Override
    public String process(String request) {
        String ret = null;
        if (dict.get(request) == null){
            ret = "该词无法查询~";
        }else {
            ret = dict.get(request);
        }
        return ret;
    }

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

关于TCP流套接字编程的注意事项:

1.由于TCP的有连接的,所以需要先建立连接,当accept到了一个客户端建立连接的请求,就会创建一个新的线程,并且将accept到的客户端构建成一个Socket对象,称为ClientSocket,后续服务器和客户端之间的交互,都是通过clientSocket来完成,ServerSocket只需要将客户端和服务器接通,剩余的工作就交由Socket对象来进行。

2.

 

 

举报

相关推荐

0 条评论