Java语言基础网络编程

网络模型

OSI(Open System Interconnection 开放系统互连)参考模型
——(底层到上层)物理层,数据链路层,网络层,传输层,会话层,表示层,应用层
TCP(传输控制协议):传输效率低,可靠性强。用于传输的可靠性要求高,数据量大的数据。
UDP(用户数据报协议):与TCP特性相反,用于传输可靠性要求不高,数据量小的数据。
TCP/IP参考模型
    ——主机至网络,网络层,传输层,应用层

网络通讯要素
IP地址(InteAddress)
    网络中设备的标识
不易记忆,可用主机名
本机回环地址:127.0.0.1 主机名:localhost
端口号
    用于标识进程的逻辑地址,不同进程的标识
    有效端口:0~65535,其中0~1024系统是使用或保留端口
传输协议
    通讯的协议
    常见协议:TCP、UDP
UDP
    将数据及源和目的封装到数据包中,不需要建立连接;
    每个数据包的大小限制在64K以内;
    因无连接,是不可靠协议;
    不需要建立连接,速度快。
TCP
    建立连接,形成传输数据的通道;
    在连接中进行大量数据传输;
    通过三次握手完成连接,是可靠协议;
    必须建立连接,效率稍低。

public class InetAddress
extends Object
implements Serializable
此类表示互联网协议 (IP) 地址。

static InetAddress getLocalHost() ;//返回本地主机。

InetAddress方法示例:

package cn.itcast.net.p1.ip;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class IPDemo {

    /**
     * @param args
     * @throws UnknownHostException
     */
    public static void main(String[] args) throws UnknownHostException {

        // 获取本地主机IP地址对象
        InetAddress ip = InetAddress.getLocalHost();

        // 获取其他主机的IP地址对象
        // ip=InetAddress.getByName("192.168.0.101");
        ip = InetAddress.getByName("www.baidu.com");

        System.out.println(ip.getHostAddress());
        System.out.println(ip.getHostName());
    }

}


Socket
Socket就是为网络服务提供的一种机制;
通信的两端都有Socket;
网络通信其实就是Socket间的通信;
数据在两个Socket间通过IO传输。

UDP传输
DatagramSocket和DatagramPacket

UDP传输示例:

package cn.itcast.net.p2.udp;

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

public class UDPSendDemo {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {

        System.out.println("发送端启动......");
        /*
         * 创建UDP传输的发送端
         * 思路:
         * 1.建立UDP的Socket服务;
         * 2.将要发送的数据封装到数据包中;
         * 3.通过UDP的Socket服务,将数据包发送出去;
         * 4.关闭Socket服务。
         */
        // 1.UDP的Scoket服务,使用DatagramSocket对象.
        DatagramSocket ds = new DatagramSocket(8888);

        // 2.将要发送的数据封装到数据包中。
        String str = "UDP传输演示...";
        // 使用DatagramPacket将对象封装到包中
        byte[] buf = str.getBytes();

        DatagramPacket dp = new DatagramPacket(buf, buf.length,
                InetAddress.getByName("192.168.0.101"), 10000);
        
        //3.通过UDP的Socket服务将数据包发送出去。使用send方法
        ds.send(dp);
        
        //4.关闭资源
        ds.close();
        
    }

}

package cn.itcast.net.p2.udp;

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

public class UDPReceiveDemo {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        
        System.out.println("接收端启动......");
        /*
         * 建立UDP接收端的思路:
         * 1.建立UDP的Socket服务;因为要接收数据,必须明确端口号;
         * 2.创建数据包,用于存储接收到的数据,方便用数据包对象的方法解析数据;
         * 3.使用Socket服务的receive方法将接收到的方法存储到数据包中;
         * 4.通过数据包的方法解析数据包中的数据;
         * 5.关闭资源。
         */
        //1.建立UDP的Socket服务
        DatagramSocket ds=new DatagramSocket(10000);
        
        //2.创建数据包
        byte[] buf=new byte[1024];
        DatagramPacket dp=new DatagramPacket(buf, buf.length);
        
        //3.使用接收方法见数据存储到数据包中
        ds.receive(dp);//阻塞式
        
        //4.通过数据包对象的方法,解析其中的数据:地址、端口、数据内容等;
        String ip=dp.getAddress().getHostAddress();
        int port=dp.getPort();
        String text=new String (dp.getData(),0,dp.getLength());
        
        System.out.println(ip+":"+port+":"+text);
        ds.close();
    }

}


基于多线程的聊天程序:

package cn.itcast.net.p3.chat;

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

public class ChatDemo {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        
        DatagramSocket send=new DatagramSocket();
        
        DatagramSocket rece=new DatagramSocket(10001);
        new Thread(new Send(send)).start();
        new Thread(new Rece(rece)).start();
    }

}

package cn.itcast.net.p3.chat;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 发送端
 * @author chenchong
 *
 */
public class Send implements Runnable {

    private DatagramSocket ds;

    public Send(DatagramSocket ds) {
        this.ds = ds;
    }

    @Override
    public void run() {
        
        try {
            BufferedReader bufr = new BufferedReader(new InputStreamReader(
                    System.in));
            String line = null;
            
            while ((line = bufr.readLine()) != null) {
                byte[] buf = line.getBytes();

                DatagramPacket dp = new DatagramPacket(buf, buf.length,
                        InetAddress.getByName("192.168.0.255"), 10001);//IP地址末尾为255,则发送广播
                ds.send(dp);
                if ("886".equals(line))
                    break;
                }
                ds.close();
        } catch (Exception e) {
        }
    }

}

package cn.itcast.net.p3.chat;

import java.net.DatagramPacket;
import java.net.DatagramSocket;


/**
 * 接收端
 * @author chenchong
 *
 */
public class Rece implements Runnable {

    private DatagramSocket ds;

    public Rece(DatagramSocket ds) {
        this.ds = ds;
    }

    @Override
    public void run() {
        try {
            while (true) {

                byte[] buf = new byte[1024];
                DatagramPacket dp = new DatagramPacket(buf, buf.length);

                ds.receive(dp);// 阻塞式

                String ip = dp.getAddress().getHostAddress();
                int port = dp.getPort();
                String text = new String(dp.getData(), 0, dp.getLength());

                System.out.println(ip + ":" + port + ":" + text);

                if (text.equals("886")) {
                    System.out.println(ip + ".....退出聊天室");
                }
                // ds.close();//此处如果关闭资源,则只接收一次
            }
        } catch (Exception e) {
        }
        ds.close();
    }

}

TCP传输
建立客户端和服务器端;
建立连接后,通过Socket中的IO流进行数据的传输;
关闭Socket。
客户端与服务器端是两个独立的应用程序。

public class Socket extends Object
此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
public class ServerSocket extends Object
此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果

TCP示例:

package cn.itcast.net.p4.tcp;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        
        /*
         *服务端接收客户端发送过来的数据,并打印在控制台上 
         * 
         * 建立TCP服务端:
         * 1.创建服务端Socket服务,通过ServerSocket对象;
         * 2.服务端必须对外提供一个端口,否则客户端无法连接;
         * 3.获取连接过来的客户端对象;
         * 4.通过客户端对象获取Socket流读取客户端发送的数据,
         *     并打印在控制台上;
         * 5.关闭资源:关客户端;关服务端。
         */
        
        //1.创建服务端对象
        ServerSocket ss=new ServerSocket(10002);
        
        //2.获取连接过来的服务端对象
        Socket s=ss.accept();
        
        //3.通过Socket对象获取输入流,读取客户端发来的数据
        InputStream in=s.getInputStream();
        
        String ip=s.getInetAddress().getHostAddress();
        
        //4.获取客户端发来的数据
        byte[] buf=new byte[1024];
        
        int len=in.read(buf);
        String text=new String(buf,0,len);
        System.out.println(ip+":"+text);
        
        //5.关闭资源
        s.close();
        ss.close();
        
        
    }

}

package cn.itcast.net.p4.tcp;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class ClientDemo {

    /**
     * @param args
     * @throws IOException
     * @throws UnknownHostException
     */
    public static void main(String[] args) throws UnknownHostException,
            IOException {
        /*
         * 需求:客户端发送数据到服务端
         * 
         * TCP传输,客户端的建立
         * 1.创建TCP客户端Socket服务,使用的是Socket对象,
         * 建议该对象一创建就明确目的地(要连接的主机);
         * 2.如果连接建立成功,说明数据传输通道已建立;
         * 通道就是Socket流,是底层建立的,这里既有输入又有输出。
         * 想要输入或者输出流对象,可以找Socket流获取;
         * 可以通过getOutputStream()和getInputStream()来获取两个字节流;
         * 3.使用输出流,将数据写出;
         * 4.关闭资源。
         */

        // 创建客户端Socket服务
        Socket socket = new Socket("192.168.0.101", 10002);

        // 获取Socket流中的输出流
        OutputStream out = socket.getOutputStream();

        // 使用输出流将指定的数据写出去
        out.write("TCP演示:这里是客户端......".getBytes());

        // 关闭资源(将连接断开)
        socket.close();
    }

}

服务端、客户端交互

package cn.itcast.net.p4.tcp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo2 {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        ServerSocket ss=new ServerSocket(10002);
        
        Socket s=ss.accept();//阻塞式
        
        InputStream in=s.getInputStream();
        
        String ip=s.getInetAddress().getHostAddress();
        
        byte[] buf=new byte[1024];
        
        int len=in.read(buf);
        String text=new String(buf,0,len);
        System.out.println(ip+":"+text);
        
        //使用客户端Socke对象的输出流给客户端返回数据
        OutputStream out=s.getOutputStream();
        out.write("server收到".getBytes());
        
        //5.关闭资源
        s.close();
        ss.close();
        
        
    }

}

package cn.itcast.net.p4.tcp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class ClientDemo2 {

    /**
     * @param args
     * @throws IOException
     * @throws UnknownHostException
     */
    public static void main(String[] args) throws UnknownHostException,
            IOException {

        Socket socket = new Socket("192.168.0.101", 10002);

        OutputStream out = socket.getOutputStream();

        out.write("TCP演示:这里是客户端......".getBytes());

        // 读取服务端返回的数据使用Socket读取流
        InputStream in = socket.getInputStream();

        byte[] buf = new byte[1024];
        int len = in.read(buf);
        String text = new String(buf, 0, len);
        System.out.println(text);

        // 关闭资源(将连接断开)
        socket.close();
    }

}


文本转换客户端

package cn.itcast.net.p5.tcptest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class TransServer {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {

        /*
         * 转换服务端:
         * 分析:
         * 1.ServerSocket服务;
         * 2.获取Socket对象;
         * 3.源:Socket,读取客户端发过来需要转换的数据;
         * 4.目的:显示在服务端控制台上;
         * 5.将数据转成大写发回客户端;
         * 6.关闭资源
         */

        // 1.ServerSocket
        ServerSocket ss = new ServerSocket(10004);

        // 2.获取Socket对象
        Socket s = ss.accept();

        // 获取ip
        String ip = s.getInetAddress().getHostAddress();
        System.out.println(ip + ".....connected.");

        // 3.获取Socket读取流,装饰
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(
                s.getInputStream()));

        // 4.获取Socket输出流并装饰
        PrintWriter out = new PrintWriter(s.getOutputStream(), true);

        String line = null;
        while ((line = bufIn.readLine()) != null) {// 读取到数据,如果没有读到换行标记,则认为没有读取完毕
            System.out.println(line);
            out.println(line.toUpperCase());
            
            // out.print(line.toUpperCase());//没有换行标记
            // out.flush();//在不设置自动刷新时,需要调用flush()方法
        }
        s.close();
        ss.close();
    }

}

package cn.itcast.net.p5.tcptest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TransClient {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {

        /*
         * 思路:
         * 客户端:
         * 1.需要Socket端点;
         * 2.客户端的数据源:键盘;
         * 3.客户端的目的:Socket;
         * 4.接收服务端的数据,源:socket;
         * 5.将数据打印出来,目的:控制台;
         * 6.在这些流中操作的数据,都是文本数据。
         * 
         * 转换客户端:
         * 1.创建Socket客户端对象;
         * 2.获取键盘录入;
         * 3.将录入的信息发送给Socket输出流;
         */

        // 1.创建Socket对象
        Socket socket = new Socket("192.168.0.101", 10004);

        // 2.获取键盘录入
        BufferedReader bufr = new BufferedReader(new InputStreamReader(
                System.in));

        // 3.目的是Socket输出流
        // new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);// 自动刷新

        // 4.Socket输入流,读取服务端返回的大写数据
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(
                socket.getInputStream()));

        String line = null;
        while ((line = bufr.readLine()) != null) {// 读取到数据,如果没有读到换行标记,则认为没有读取完毕
            if ("over".equals(line))
                break;
            out.println(line);// 写入到PrintWriter中,需要刷新动作,才输出到Socket流中

            // out.print(line);//没有换行标记
            // out.flush();//在不设置自动刷新时,需要调用flush()方法

            // 读取服务端发回的一行大写数据
            String upperStr = bufIn.readLine();
            System.out.println(upperStr);
        }
        socket.close();// 导致服务端结束 
    }

}

如果不刷新,则数据暂存在PrintWriter中,不输出到Socket输出流,需要使用flush()方法;
如果readLine()没有读到换行标记,则任务数据没有读取完毕,继续等待;


上传文本文件

package cn.itcast.net.p6.uploadtext;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class UploadServer {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        
        //1.创建ServerSocket服务端
        ServerSocket ss=new ServerSocket(10005);
        
        //2.获取Socket对象
        Socket s=ss.accept();
        System.out.println(s.getInetAddress().getHostAddress()+"......connected.");
        
        //3.获取客户端读取流
        BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
        
        //4.创建目的文件对象
        BufferedWriter bufw=new BufferedWriter(new FileWriter("c:\\server.txt"));
        
        String line=null;
        while((line=bufIn.readLine())!=null){
            if("over".equals(line))
                break;
            bufw.write(line);
            bufw.newLine();
            bufw.flush();
        }
        
        PrintWriter out=new PrintWriter(s.getOutputStream(),true);
        out.println("上传成功");

        bufw.close();
        s.close();
        ss.close();
        
    }

}

package cn.itcast.net.p6.uploadtext;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class UploadClient {

    /**
     * @param args
     * @throws IOException
     * @throws UnknownHostException
     */
    public static void main(String[] args) throws UnknownHostException,
            IOException {

        // 1.创建Socket对象
        Socket s = new Socket("192.168.0.101", 10005);

        // 2.创建源文件对象,源是文本文件
        BufferedReader bufr = new BufferedReader(new FileReader(
                "c:\\client.txt"));
        

        // 3.目的是Socket输出流
        PrintWriter out = new PrintWriter(s.getOutputStream(), true);
        String line = null;
        while ((line = bufr.readLine()) != null) {
            out.println(line);
            out.flush();
        }
        // out.println("over");
        // 告诉服务端,客户端数据已经写完了
        s.shutdownOutput();

        // 4.读取Socket流,
        BufferedReader bufIn = new BufferedReader(new InputStreamReader(
                s.getInputStream()));

        String str = bufIn.readLine();
        System.out.println(str);

        bufr.close();
        s.close();

    }

}

服务端多线程示例:

package cn.itcast.net.p1.uploadpic;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class UploadPicServer {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {

        // 1.创建TCP的Socket服务端
        ServerSocket ss = new ServerSocket(10006);

        while (true) {
            // 2.获取客户端
            Socket s = ss.accept();

            new Thread(new UploadTask(s)).start();
        }

        // ss.close();

    }

}

package cn.itcast.net.p1.uploadpic;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class UploadTask implements Runnable {

    private Socket s;

    public UploadTask(Socket s) {
        this.s = s;
    }

    @Override
    public void run() {

        int count=0;
        String ip = s.getInetAddress().getHostAddress();
        System.out.println(ip + "......connected.");

        try {
            // 3.读取客户端发来的数据
            InputStream in = s.getInputStream();

            // 4.将读取到的数据存储到一个文件中
            File dir = new File("c:\\pic");
            if (!(dir.exists())) {
                dir.mkdirs();
            }
            File file = new File(dir, ip + ".jpg");
            
            //如果文件已经存在于服务器端
            while(file.exists()){
                file =new File(dir,ip+"("+(++count)+").jpg");
            }
            
            FileOutputStream fos = new FileOutputStream(file);

            byte[] buf = new byte[1024];
            int len = 0;
            while ((len = in.read(buf)) != -1) {
                fos.write(buf, 0, len);
            }

            // 给客户端发回:上传成功
            OutputStream out = s.getOutputStream();
            out.write("上传成功".getBytes());

            fos.close();
            s.close();
        } catch (IOException e) {
        }
    }

}

package cn.itcast.net.p1.uploadpic;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class UploadPicClient {

    /**
     * @param args
     * @throws IOException
     * @throws UnknownHostException
     */
    public static void main(String[] args) throws UnknownHostException,
            IOException {

        // 1.创建客户端
        Socket s = new Socket("192.168.0.101", 10006);

        // 2.读取客户端要上传的图片文件
        FileInputStream fis = new FileInputStream("c:\\0.jpg");

        // 3.获取Socket输出流,将读到的图片数据发送给服务端
        OutputStream out = s.getOutputStream();

        byte[] buf = new byte[1024];

        int len = 0;
        while ((len = fis.read(buf)) != -1) {
            out.write(buf, 0, len);
        }
        // 告诉服务端,数据发送完毕
        s.shutdownOutput();

        // 读取服务端发回的内容
        InputStream in = s.getInputStream();
        byte[] bufIn = new byte[1024];
        int lenIn = in.read(bufIn);
        String text = new String(bufIn, 0, lenIn);
        System.out.println(text);

        fis.close();
        s.close();
    }

}


常见客户端和服务器
最常见的客户端:
    浏览器:IE
最常见的服务端:
    服务器:Tomcat

客户端和服务器端原理
1.自定义服务端,使用已有的客户端IE,了解客户端给服务器端发送了什么请求。

GET / HTTP/1.1            请求行:请求方式        /myweb/index.html 请求的资源路径 http协议版本
请求消息头,属性名:属性值
Accept: text/html, application/xhtml+xml, */*       
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)
Accept-Encoding: gzip, deflate
Host: 192.168.0.101:9090
Connection: Keep-Alive
//请求头和请求体之间有一个空行,此行为空行
//请求体

模拟浏览器
//服务器发回的应答消息
HTTP/1.1 200 OK    //应答行    http的协议版本,应答状态码,应答状态描述信息

//应答消息头。属性名:属性值
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"1807-1345210068813"
Last-Modified: Fri, 17 Aug 2012 13:27:48 GMT
Content-Type: text/html
Content-Length: 1807
Date: Fri, 17 Aug 2012 13:28:20 GMT
Connection: close
//空行
//应答体
<font color='red' size='7'>欢迎光临</font>


public final class URL extends Objectimplements Serializable
类 URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。

InputStream openStream()
打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。
打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。此方法是下面方法的缩写:
     openConnection().getInputStream()

package cn.itcast.net.p2.ie_server;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class URLDemo {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {

        String str_url = "http://192.168.0.101:8080/myweb/2.html?name=lisi";

        URL url = new URL(str_url);

        // System.out.println("getProtocol:"+url.getProtocol());
        // System.out.println("getHost:"+url.getHost());
        // System.out.println("getPath:"+url.getPath());
        // System.out.println("getFile:"+url.getFile());
        // System.out.println("getPath:"+url.getPath());
        // System.out.println("getQuery:"+url.getQuery());

        // 获取url对象的Url连接器对象,将连接封装成了对象:java中内置的可以解析具体协议的对象+socket
        URLConnection conn = url.openConnection();
        
        String value=conn.getHeaderField("Content-Type");//text/html

        System.out.println(value);
        
//        System.out.println(conn);
        // 输出:sun.net.www.protocol.http.HttpURLConnection:http://192.168.0.101:8080/myweb/2.html?name=lisi

        
        // InputStream in=url.openStream();//打开到此 URL 的连接并返回一个用于从该连接读入的InputStream
        // 打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。此方法是下面方法的缩写:openConnection().getInputStream()
        InputStream in=conn.getInputStream();
        
         byte[] buf=new byte[1024];
         int len=in.read(buf);
        
         String text=new String(buf,0,len);
        
         System.out.println(text);//只有应答体,没有消息头
        
         in.close();

    }

}


网络架构
1.C/S    Client/Server   
        缺点:
        该结构的软件,客户端和服务端都需要编写;
        开发成本较高,维护较为麻烦;
        优点:
        客户端在本地,可以分担一部分运算;

2.B/S    Browser/Server   
        缺点:
        所有运算都在服务器端完成;
        优点:
        该结构的软件,只开发服务器端,不开发客户端;因为客户端直接由浏览器取代;
        开发成本相对较低,维护更为简单;

原文地址:https://www.cnblogs.com/chenchong/p/2645328.html