网络编程原理

目录

为什么需要网络编程

什么是网络编程

Socket套接字

简单认识UDP 和 TCP协议

基于UDP编写一个简单的客户端服务器网络通信程序

DatagramSocket

DatagramPacket 

UDP版本回显服务器客户端代码

 端口冲突

基于TCP编写一个简单的客户端服务器网络通信程序

ServerSocket

Socket

TCP版本的回显服务器客户端代码

代码优化


为什么需要网络编程

获取更加丰富的网络资源.

用户在浏览器中,打开在线视频网站,观看视频.这一过程,实质上是通过网络,获取到网络上的一个视频资源.与本地打开视频文件类似,只不过视频文件这个资源是来自于网络.

相对于本地资源来说,网络提供了更为丰富的网络资源.

网络资源,就是在网络中可以获取的各种数据资源.而所有的数据资源,都是通过网络编程来进行数据传输的.


什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输).

当然,我们只要满足进程不同即可,所以即便是同一个主机,只要是不同的进程,基于网络来传输数据,也是属于网络编程的.


Socket套接字

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

Socket 被译为套接字.

网络编程的核心就是Socket API,这是操作系统给应用程序提供的网络编程API.Socket API是站在传输层的角度和应用层直接进行交互.

可以认为socket api 是和传输层密切相关的.

传输层里,提供了两个最核心的协议,UDP 和 TCP,因此socket api 也提供了;两种风格 UDP 和TCP.


简单认识UDP 和 TCP协议

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

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

连接:打电话就是有连接的,通信双方需要建立了连接才能进行通信,连接的建立需要对方来"接受",如果连接没建立好,就无法进行通信;发短信/发微信 就是无连接的,直接发送即可.

可靠:网络环境天然是复杂的,不可能保证传输的数据100%到达.可靠传输,就是发送方能知道自己的消息是发送过去了,还是在传输过程中丢了.

需要注意的是,可靠不可靠和有没有连接是没有任何关系的,比如带有已读功能的app如钉钉,就是无连接 可靠传输.

面向字节流:数据传输和文件读写类似,是"流式"的.

面向数据报:数据传输是以一个个的"数据报"为基本单位.(一个数据报可能是若干个字节,是带有一定的格式的)

全双工:一个通信通道,可以双向传输,即可以发送,也可以接收.(半双工,是单向传输的,类似于家里的水管,只能往外流水,不能往里灌水)

为什么UDP 和TCP都是全双工的?

网线看起来像是水管,所以容易误认为是半双工的,其实不是,一根网线里其实有八根线.类似于八车道的公路,一半是往一个方向的,一半是往反方向的.


基于UDP编写一个简单的客户端服务器网络通信程序

Java标准库里基于UDP socket提供了两个最核心的类DatagramSocket和DatagramPacket,通过这两个类就可以实现基于UDP的网络编程.

DatagramSocket

使用这个类,表示一个socket对象.
在操作系统中,把这个socket对象也是当成一个文件来处理的,相当于是文件描述符表上的一项.
普通的文件,对应的硬件设备是硬盘.socket文件对应的硬件设备是网卡.这也符合操作系统的一切皆文件的设计理念.
构造方法
第一个版本没有指定端口,系统则会自动分配一个空闲的端口.
第二个版本是要传入一个端口号,此时就是让当前的socket对象和这个指定的端口关联起来,本质上说,不是进程和端口建立联系,而是进程中的socket对象和端口建立了联系.

 方法:

 receive方法相当于传入一个空的对象,receive方法内部,会对参数的这个空对象进行内容填充,从而构造出数据,此处的参数也是一个"输出型参数".

DatagramPacket 

这个类表示UDP传输的一个报文.构造这个对象,可以指定一些具体的数据进去.
构造方法

 使用SocketAddress类表示IP+port

方法


UDP版本回显服务器客户端代码

编写一个最简单的UDP版本的客户端服务器程序:回显服务器(ehco server).

 一个普通的服务器:收到请求,根据请求计算相应,返回响应.

上述是一个普通服务器的工作过程,而echo server省略了其中的"计算请求计算相应",请求是什么,就返回什么.(这个代码没有实际的业务,也没有什么太大的作用和意义,只是为了展示socket api的基本用法)

package network;

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


//UDP版本的回显服务器
public class UdpEchoServer {
    //网络编程,本质上是要操作网卡.
    //但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做"socket"这样的文件来抽象表示网卡
    //因此进行网络通信,势必需要现有一个socket对象
    private DatagramSocket socket = null;

    //对于服务器来说,创建 socket 对象的同时,要让它绑定上一个具体的端口号.
    //服务器一定要关联上一个具体的端口号!
    //服务器在网络传输中,是被动的一方,如果是操作系统随机分配的端口号,此时客户端就不知道这个端口是什么了,也就无法进行通信了!!
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器不是只给一个客户端提供服务,而是服务很多客户端
        while (true){
            //只要客户端过来,就可以提供服务
            //1.读取客户端发来的请求是什么
             //receive 方法的参数是一个输出型参数,需要先构造好一个空白的 DatagramPacket 对象,交给receive来进行填充
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.根据请求计算响应,由于此处是回显服务器,响应和请求相同.
            String response = process(request);
            //3.把相应写回到客户端. send 方法的参数也是 DatagramPacket.需要把这个Packet 对象构造好.
            DatagramPacket responsePacket  = new DatagramPacket(request.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印当前这次请求响应的处理中间结果
            System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
                    request,response);
        }
    }
    ///这个方法就表示"根据请求计算相应"
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        //端口号的指定,可以随便指定
        //1024-65535 这个范围里随便挑个数字
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();

    }


}
package network;

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

//UDP版本的回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;

    private String serverIp = null;
    private int serverPort= 0;
    //一次通信,需要两个ip,两个端口.
    //客户端的ip 是127.0.0.1
    //客户端的port 是系统自动分配的.
    //服务器的ip和端口也要告诉客户端,才能顺利的把消息发给服务器.
    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        //构造这个socket 对象,不需要显式绑定一个端口(让操作系统自动分配一个端口,随机挑一个空闲的)
        //端口号用来标识//区分一个进程
        //因此不允许一个端口同时被多个进程使用(前提是同一个主机)
        //客户端如果显式指定窗口,可能就和客户端电脑上的其他程序的端口冲突了.这一冲突就可能导致程序无法正常通信了.
        //那么为什么服务器指定端口不怕重复呢?
        //服务器是程序员自己手里的机器,上面运行什么,都是程序员可控的,程序员就可以安排哪个程序用哪个端口,这是可控的.
        //客户端的机器在用户手里,不同用户手里的机器,千奇百怪,上面运行着什么样的程序,也各有不同,这是不可控的.
        socket = new DatagramSocket();
        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();
            if (request.equals("exit")){
                System.out.println("goodbye!");
                break;
            }
            //2.构造成UDP请求,并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            //3.读取服务器的UDP响应,并解析
            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();
    }
}

 端口冲突

端口冲突会报这样的异常: 


基于TCP编写一个简单的客户端服务器网络通信程序

TCP提供的API主要是两个类

ServerSocket和Socket

ServerSocket

专门给服务器使用的Socket对象.

Socket

是既会给客户端使用,也会给服务器使用.

需要注意的是:TCP不需要一个类来表示"TCP数据报",TCP不是以数据报为单位进行传输的,是以字节的方式,流式传输.

ServerSocket

构造方法:

方法

 Socket

 Socket在服务器这边是由accept返回的;在客户端这边,是我们代码里构造的.构造的时候指定一个IP和端口号.(此处指定的IP和端口号是服务器的IP和端口),有了这个信息,就能和这个服务器建立连接了.


TCP版本的回显服务器客户端代码

package network;

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.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){
            //使用这个 clientSocket 和具体的客户端进行交流
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    //使用这个方法来处理一个连接
    //这一个连接对应到一个客户端,但是这里可能会涉及到多次交互
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //基于上述Socket对象和客户端进行通信
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //由于要处理多个请求和响应,也是使用循环来进行
            while (true){
                //1.读取请求
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()){
                    //没有下个数据,说明读完了.(客户端关闭了连接)
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //此处使用next 是一直读取到换行符/空格//其他空白符结束,但是最终返回的结果里不包含上述空白符
                String request = scanner.next();
                //2.根据请求,构造响应
                String response = process(request);
                //3.返回响应结果
                // OutputStream 没有write String 这样的功能,可以把String里的字节数组拿出来,进行写入
                PrintWriter printWriter = new PrintWriter(outputStream);
                //此处使用 println 来写入,让结果中带有一个换行,方便对端来接受解析;
                printWriter.println(response);
                //flush用来刷新缓冲区,保证当前写入的数据确实是发送出去了
                printWriter.flush();

                System.out.printf("[%s:%d] req: %s;resp: %s \n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                            request,response);
            }
        }
    }

    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}
package network;

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

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // Socket 构造方法, 能够识别 点分十进制格式的 IP 地址. 比 DatagramPacket 更方便.
        // new 这个对象的同时, 就会进行 TCP 连接操作.
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 先从键盘上读取用户输入的内容
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }
                // 2. 把读到的内容构造成请求, 发送给服务器.
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                // 此处加上 flush 保证数据确实发送出去了.
                printWriter.flush();
                // 3. 读取服务器的响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                // 4. 把响应内容显示到界面上
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

代码优化

当前代码中,还有一个非常重要的问题!我们当前的服务器,同一时刻只能处理一个连接(只能给一个客户端提供服务),这是不合理的.

当客户端连接上服务器后,代码就执行到了processConnection这个方法里的while循环中了,此时就意味着,只要循环不结束,processConnection方法就结束不了,进一步的也就无法第二次调用到accept.

那我们基于上述问题,就可以使用多线程的方法来解决.

主线程,专门负责进行accept,每次收到一个连接,创建新线程,由这个新的线程负责处理这个新的客户端.

每个线程是独立的执行流,每个独立的执行流,是各自执行各自的逻辑,彼此之间是并发的关系.

 这样就把processConnection方法的执行从主线程中拿出来了,交给新的线程去处理.

如果我们的服务器,客户端很多,有很多客户端频繁的建立连接,就需要频繁的创建/销毁线程,针对此问题,我们可以使用线程池来解决.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值