黑马程序员_Java基础:网络编程总结
------- android培训、java培训、期待与您交流! ----------
Java语言是在网络环境下诞生的,它是第一个完全融入网络的语言,虽然不能说它是对支持网络编程做得最好的语言,但是必须说是一种对于网络编程提供良好支持的语言。这归功于java的自身优势:
1.java语言与生俱来就是与平台无关的,有良好的跨平台性,所以运行在不同在平台上的java程序能够方便地进行网络通信。
2.java语言具有良好的安全机制,可以对程序进行权限检查,这对网络程序是至关重要的。
3.JDK中有丰富的网络类库,大大简化了网路变成的开发过程。
一、网络编程概念
网络编程就是两个或多个设备之间的数据交换,其实更具体的说,网络编程就是两个或多个程序之间的数据交换,和普通的单机程序相比,网络程序最大的不同就是需要交换数据的程序运行在不同的计算机上,这样就造成了数据交换的复杂。java的网络编程基础实际上是在操作网络参考模型中的网络层和传输层。
以下是网络的OSI参考模型:

二、网络通讯要素
我们既然要进行网络编程,那么必须了解网络中的通讯要素。
1.IP地址:InetAddress,它是网络中的设备标识,平时我们访问网址的域名其实是都要先解析成IP地址然后再去访问的,本地回环地址是127.0.0.1,或主机名localhost。

2.端口号:port,用于标识进程的逻辑地址,有效的端口号为0~65535,其中0~1024系统使用或保留端口,我们平常编程中通常使用10000以后的端口号。
3.传输协议,是通讯的规则,常见的有UDP和TCP。
UDP特点:
(1)将数据及源和目的封装成数据包,不需建立连接。
(2)每个数据的大小都限制在64K内。
(3)因无连接,是无可靠的协议(有丢失数据的可能)。
(4)不需要建立连接,速度快。
TCP特点:
(1)建立连接,形成传输数据的通道。
(2)在连接中进行大数据传输。
(3)通过三次握手完成连接,是可靠的协议。
(4)必须建立连接,效率会稍低。

三、网络编程的使用方法
1.使用UDP协议
在Java API中,实现UDP方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:
(1) DatagramSocket
DatagramSocket类实现“网络连接”,包括客户端网络连接和服务器端网络连接。虽然UDP方式的网络通讯不需要建立专用的网络连接,但是毕竟还是需要发送和接收数据,DatagramSocket实现的就是发送数据时的发射器,以及接收数据时的监听器的角色。类比于TCP中的网络连接,该类既可以用于实现客户端连接,也可以用于实现服务器端连接。
(2)DatagramPacket
DatagramPacket类实现对于网络中传输的数据封装,也就是说,该类的对象代表网络中交换的数据。在UDP方式的网络编程中,无论是需要发送的数据还是需要接收的数据,都必须被处理成DatagramPacket类型的对象,该对象中包含发送到的地址、发送到的端口号以及发送的内容等。其实DatagramPacket类的作用类似于现实中的信件,在信件中包含信件发送到的地址以及接收人,还有发送的内容等,邮局只需要按照地址传递即可。在接收数据时,接收到的数据也必须被处理成DatagramPacket类型的对象,在该对象中包含发送方的地址、端口号等信息,也包含数据的内容。和TCP方式的网络传输相比,IO编程在UDP方式的网络编程中变得不是必须的内容,结构也要比TCP方式的网络编程简单一些。
具体使用方法请看一下例子:(以下例子为了方便阅读,异常都做抛处理)
接收端:
import java.net.DatagramPacket;
import java.net.DatagramSocket; /*
需求:
定义一个应用程序,用于接收udp协议传输的数据并处理的。 定义udp的接收端。
思路:
1,定义udpsocket服务。通常会监听一个端口。其实就是给这个接收网络应用程序定义数字标识。
方便于明确哪些数据过来该应用程序可以处理。 2,定义一个数据包,因为要存储接收到的字节数据。
因为数据包对象中有更多功能可以提取字节数据中的不同数据信息。
3,通过socket服务的receive方法将收到的数据存入已定义好的数据包中。
4,通过数据包对象的特有功能。将这些不同的数据取出。打印在控制台上。
5,关闭资源。 */
public class UdpReceive {
public static void main(String[] args) throws Exception {
//1,创建udp socket,设置监听端点。
DatagramSocket ds = new DatagramSocket(10000);
while(true) {
//2,定义数据包。用于存储数据。
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf,buf.length); //3,通过服务的receive方法将收到数据存入数据包中。
ds.receive(dp);//阻塞式方法。 //4,通过数据包的方法获取其中的数据。
String ip = dp.getAddress().getHostAddress();
String data = new String(dp.getData(),0,dp.getLength());
int port = dp.getPort();
System.out.println(ip+"..."+data+"..."+port);
}
//5,关闭资源,这里假设需要一直开启接收,所以省略。
//ds.close();
}
}
发送端:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress; /*
需求:通过udp传输方式,将一段文字数据发送出去。,
定义一个udp发送端。
思路:
1,建立updsocket服务。
2,提供数据,并将数据封装到数据包中。
3,通过socket服务的发送功能,将数据包发出去。
4,关闭资源。 */
public class UdpSend {
public static void main(String[] args) throws Exception {
//1,创建udp服务。通过DatagramSocket对象。
DatagramSocket ds = new DatagramSocket(8888); //2,确定数据,并封装成数据包。DatagramPacket(byte[] buf, int length, InetAddress address, int port)
byte[] buf = "Hello,I'm udp".getBytes();
DatagramPacket dp = new DatagramPacket(buf,buf.length,InetAddress.getByName("192.168.1.101"),10000); //目的地IP地址和端口。 //3,通过socket服务,将已有的数据包发送出去。通过send方法。
ds.send(dp); //4,关闭资源。
ds.close();
}
}
接收端的结果是:
192.168.1.101...Hello,I'm udp...8888
2.使用TCP协议
在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。正是由于这种高度的封装,一方面简化了Java语言网络编程的难度,另外也使得使用Java语言进行网络编程时无法深入到网络的底层,所以使用Java语言进行网络底层系统编程很困难。但是由于Java语言的网络编程比较简单,所以还是获得了广泛的使用。
具体使用方法请看以下例子:
服务端:
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket; /*
需求:定义端点接收数据并打印在控制台上。
服务端:
1,建立服务端的socket服务。ServerSocket();
并监听一个端口。
2,获取连接过来的客户端对象。
通过ServerSokcet的 accept方法。没有连接就会等,所以这个方法阻塞式的。
3,客户端如果发过来数据,那么服务端要使用对应的客户端对象,并获取到该客户端对象的读取流来读取发过来的数据。
并打印在控制台。
4,关闭服务端。(可选)
*/
public class TcpServer {
public static void main(String[] args) throws Exception {
//建立服务端socket服务。并监听一个端口。
ServerSocket ss = new ServerSocket(10001); //通过accept方法获取连接过来的客户端对象。
while(true) {
Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+".....connected"); //获取客户端发送过来的数据,那么要使用客户端对象的读取流来读取数据。
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
s.close();//关闭客户端.
}
//ss.close();
}
}
客户端:
import java.io.*;
/*
客户端,
通过查阅socket对象,发现在该对象建立时,就可以去连接指定主机。
因为tcp是面向连接的。所以在建立socket服务时,
就要有服务端存在,并连接成功。形成通路后,在该通道进行数据的传输。 需求:给服务端发送给一个文本数据。
*/
import java.net.*;
public class TcpClient
{
public static void main(String[] args) throws Exception
{
//创建客户端的socket服务。指定目的主机和端口
Socket s = new Socket("192.168.1.101",10001); //为了发送数据,应该获取socket流中的输出流。
OutputStream out = s.getOutputStream();
out.write("Hello,I'm tcp".getBytes());
s.close();
}
}
服务端运行结果是:
192.168.1.101.....connected
Hello,I'm tcp
四、网络编程的应用
1.例子一(设计一个聊天程序,能发送和接收信息(UDP)):
import java.io.*;
import java.net.*;
/*
编写一个聊天程序。
有收数据的部分,和发数据的部分。
这两部分需要同时执行。
那就需要用到多线程技术。
一个线程控制收,一个线程控制发。 因为收和发动作是不一致的,所以要定义两个run方法。
而且这两个方法要封装到不同的类中。
*/
class Send implements Runnable {
private DatagramSocket ds;
public Send(DatagramSocket ds) {
this.ds = ds;
} 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.1.101"),10002);
ds.send(dp);
if("886".equals(line))
break;
}
} catch (Exception e) {
throw new RuntimeException("发送失败");
} finally {
ds.close();
}
}
} class Rece implements Runnable {
private DatagramSocket ds;
public Rece(DatagramSocket ds) {
this.ds = ds;
} public void run() {
try {
BufferedWriter bufw = new BufferedWriter(new PrintWriter(System.out));
while (true) {
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf,buf.length);
ds.receive(dp); String ip = dp.getAddress().getHostAddress();
//注意要限定好转成字符串的字节数组长度,否则字符串的后面有很多空格,
//多了这些空格,后面的判断标记跳出循环也无法其作用了。
String data = new String(dp.getData(),0,dp.getLength());
bufw.write(ip+"..."+data);
bufw.newLine();
if ("886".equals(data)) {
bufw.write(ip+"...离开聊天室");
bufw.flush();
break;
}
bufw.flush();
}
} catch (Exception e) {
throw new RuntimeException("接收失败");
} finally {
ds.close();
}
}
} public class ChatTest {
public static void main(String[] args) throws Exception {
DatagramSocket sendSocket = new DatagramSocket();
DatagramSocket receiveSocket = new DatagramSocket(10002); //注意可能会因为端口已被占用发生异常。 new Thread(new Send(sendSocket)).start();
new Thread(new Rece(receiveSocket)).start();
}
}
2.例子二(设计一个网络服务将收到的信息转成大写返回(TCP)):
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket; /*
需求:建立一个文本转换服务器。
客户端给服务端发送文本,服务端会将文本转换成大写再返回给客户端。
而且客户端可以不断的进行文本转换。当客户端输入over时,转换结束。 服务端:
源:socket读取流。
目的:socket输出流。
都是文本,装饰。
*/ public class TransServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10004); Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"...connected"); //读取socket读取流中的数据。
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream())); //目的:socket输入流。将大写数据写入发哦socket输出流,并发送给客户端。
PrintWriter out = new PrintWriter(s.getOutputStream(),true); String line = null;
while ((line=bufIn.readLine())!=null) {
System.out.println(line);
out.println(line.toUpperCase());//因为定义时模式带有刷新,所以这里换行和刷新省去。
}
s.close();
ss.close();
}
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket; /*
分析:
客户端:
既然是操作设备上的数据,那么久可以使用io技术,并按照io的操作规律来思考。
源:键盘录入。
目的;网络设备,网络输出流。
而且操作的是文本数据,可以现则字符流。 步骤:
1,建立服务。
2,获取键盘录入。
3,将数据发送给服务端。
4,获取服务端返回的大写数据。
5,结束,关闭资源。 都是文本数据,可以使用字符流进行操作,同时提高效率,加入缓冲。
*/
public class TransClient {
public static void main(String[] args) throws Exception {
Socket s = new Socket("192.168.1.101",10004); //定义读取键盘数据的流对象。
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in)); //定义目的,将数据写入到socket输出流,发送给服务端。
//BufferedWriter bufout = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
PrintWriter out = new PrintWriter(s.getOutputStream(),true); //定义一个socket读取流,读取服务端返回的大写信息。
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream())); String line = null;
while ((line=bufr.readLine())!=null) {
if("over".equals(line))
break;
out.println(line); String str = bufIn.readLine();
System.out.println("server:"+str);//因为定义时模式带有刷新,所以这里换行和刷新省去。
}
bufr.close();
s.close();
}
}
3.例子三(设计一个网络服务接收多个客户端上传的图片(TCP)):
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket; /*
服务端: 如果服务器处理客户请求的时候是单线程的,那么就有局限性。后来的客户端请求服务时,
如果服务器还没处理外上一个客户端的请求,那么后来的客户端只能等待,直到服务端再次执行accept方法。 为了可以让多个客户同时并发访问服务端,那么服务端最好就是将每个客户端封装到一个单独的线程中。
这样,就可以同时处理多个客户端请求。 如何定义线程?
只要明确每一个客户端要在服务端执行的代码即可,将该代码存入run方法中。
*/ class PicThread implements Runnable {
private Socket s;
PicThread(Socket s) {
this.s = s;
}
public void run() {
int count = 1;
FileOutputStream fos = null;
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"...connected");
try {
InputStream in = s.getInputStream(); File file = new File(ip+"("+(count++)+")"+".jpg");
while (file.exists())
file = new File(ip+"("+(count++)+")"+".jpg");
fos = new FileOutputStream(file);
byte[] buf = new byte[1024];
int len = 0;
while ((len=in.read(buf))!=-1)
{
fos.write(buf,0,len);
}
System.out.println("接收了一张图片");
OutputStream out = s.getOutputStream();
out.write("上传成功".getBytes());
} catch (Exception e) {
throw new RuntimeException(ip+"上传失败");
} finally {
try {
fos.close();
s.close();
} catch (Exception ex) {
throw new RuntimeException("关闭资源失败");
}
}
}
} public class PicServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10005);
while (true) {
Socket s = ss.accept();
new Thread(new PicThread(s)).start();
}
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket; class PicClient {
public static void main(String[] args) throws Exception {
if (args.length!=1) { //通过编译时输入图片路径,如果没有输入,则出现提示。
System.out.println("请选择一个jpg格式的图片上传");
return;
} File file = new File(args[0]);
if (!(file.exists() && file.isFile())) {
System.out.println("文件不存在或者不是文件类型");
return;
}
if (!(file.getName().endsWith(".jpg"))) {
System.out.println("图片格式错误,请重新选择");
return;
}
if (file.length()>1024*1024*5) {
System.out.println("文件过大,请重新选择");
return;
} Socket s = new Socket("192.168.1.101",10005);
FileInputStream fis = new FileInputStream(file);
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(); //关闭socket的输出流,相当于给服务端发送结束标识。 InputStream in = s.getInputStream();
byte[] bufIn = new byte[1024];
int num = in.read(bufIn);
System.out.println(new String(bufIn,0,num)); fis.close();
s.close();
}
}
4.例子四(设计网络服务接收客户端登录,若登录错误三次则停止(TCP))
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket; /*
客户端通过键盘录入用户名。
服务端对这个用户名进行校验。
如果该用户存在,在服务端显示xxx,已登录。
并在客户端显示xxx,欢迎光临。
如果该用户不存在,在服务端显示xxx,尝试登录。
并在客户端显示xxx,该用户不存在。
最多三次登录机会。
*/
class UserThread implements Runnable {
private Socket s;
UserThread(Socket s) {
this.s = s;
}
public void run() {
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"...connected");
BufferedReader bufr = null;
try {
for (int x=0; x<3; x++) {
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
String name = bufIn.readLine();
// 读取一个记录用户名的文件
bufr = new BufferedReader(new FileReader("user.txt"));
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
String line = null;
boolean flag = false; while((line = bufr.readLine())!=null) {
if (line.equals(name)) {
flag = true;
break;
}
}
if (flag) {
System.out.println(name+"已登录");
out.println(name+"欢迎光临");
break;
} else {
System.out.println(name+"尝试登录");
out.println(name+"用户名不存在");
}
}
}
catch (Exception e) {
throw new RuntimeException(ip+"校验失败");
} finally {
try {
if (bufr!=null) {
bufr.close();
s.close();
}
} catch (Exception ex) {
throw new RuntimeException("操作关闭失败");
}
}
}
} public class LoginServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10006);
while (true) {
Socket s = ss.accept();
new Thread(new UserThread(s)).start();
}
}
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class LoginClient {
public static void main(String[] args) throws Exception {
Socket s = new Socket("192.168.1.101",10006);
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
for (int x=0; x<3; x++) {
String line = bufr.readLine();
if(line==null)
break;
out.println(line);
String info = bufIn.readLine();
System.out.println("info"+info);
if(info.contains("欢迎"))
break;
}
bufr.close();
s.close();
}
}
5.例子五(访问网站上的资源):
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
/*
此例可以说明URL类达到走应用层的效果。
url不需关资源操作。
*/
public class URLConnectionDemo
{
public static void main(String[] args) throws Exception
{
URL url = new URL("http://www.163.com"); // 通过URL的方法获取输入流。
URLConnection conn = url.openConnection();
System.out.println(conn);
InputStream in = conn.getInputStream(); byte[] buf = new byte[1024];
int len = in.read(buf);
while ((len=in.read(buf))!=-1)
System.out.println(new String(buf,0,len));
}
}
运行结果将会把指定网站页面的源代码打印出来。
从这最后一例子可以看出,使用java.net包中的URL类可以直接完成对网站的访问请求,通过URL方法获取输入流,从而最终得到返回的信息,此类实际走的是应用层。
黑马程序员_Java基础:网络编程总结的更多相关文章
- 黑马程序员_java基础笔记(08)...GUI,网络编程,正则表达式
—————————— ASP.Net+Android+IOS开发..Net培训.期待与您交流! —————————— GUI(Graphical User Interface)(图形用户接口):用图形 ...
- 黑马程序员_Java基础视频-深入浅出精华版--PPT 文件列表
\day01\code\第一章_Java概述.ppt;\day01\resource\资料\50道编程题(有精力的同学看看).doc;\day01\resource\资料\Sun_Java程序员认证考 ...
- 黑马程序员_Java基础组成
Java语言基础组成 2.1关键字 main不是关键字,但被JVM所识别的名称. 关键字的定义和特点 定义:被Java语言赋予了特殊含义的单词. 特点:关键字中所有字母都为小写. 用于定义数据类型的关 ...
- 黑马程序员_Java基础视频-深入浅出精华版--视频列表
\day01\avi\01.01_计算机基础(计算机概述).avi; \day01\avi\01.02_计算机基础(计算机硬件和软件概述).avi; \day01\avi\01.03_计算机基础(软件 ...
- 课程2:《黑马程序员_Java基础视频-深入浅出精华版》-视频列表-
\day01\avi\01.01_计算机基础(计算机概述).avi; \day01\avi\01.02_计算机基础(计算机硬件和软件概述).avi; \day01\avi\01.03_计算机基础(软件 ...
- 黑马程序员_java基础笔记(09)...HTML基本知识、CSS、JavaScript、DOM
—————————— ASP.Net+Android+IOS开发..Net培训.期待与您交流! —————————— 基本标签(a.p.img.li.table.div.span).表单标签.ifra ...
- 黑马程序员_java基础笔记(07)...IO流
—————————— ASP.Net+Android+IOS开发..Net培训.期待与您交流!—————————— IO(InputStream,outputStream)字节流 (Reader,Wr ...
- 黑马程序员_Java基础:IO流总结
------- android培训.java培训.期待与您交流! ---------- IO流在是java中非常重要,也是应用非常频繁的一种技术.初学者要是能把IO技术的学透,java基础也就能更加牢 ...
- 黑马程序员_Java基础常识
一.基础常识 1,软件开发 1)什么是软件?软件:一系列按照特定顺序组织的计算机数据和指令的集合. 常见的软件 系统软件 如:DOS,windows,Linux等. 应用软件: 如:扫雷,迅雷,QQ等 ...
随机推荐
- git代理设置方法解决
git config --global https.proxy http://127.0.0.1:1080 git config --global https.proxy https://127.0. ...
- Android自定义控件
开发自定义控件的步骤: 1.了解View的工作原理 2. 编写继承自View的子类 3. 为自定义View类增加属性 4. 绘制控件 5. 响应用户消息 6 .自定义回调函数 一.Vie ...
- linux命令(6):rmdir 命令
rmdir命令 rmdir是常用的命令,该命令的功能是删除空目录,一个目录被删除之前必须是空的.(注意,rm - r dir命令可代替rmdir,但是有很大危险性.)删除某目录时也必须具有对父目录的写 ...
- ffmpeg 音频转换: use ffmpeg convert the audio from stereo to mono without changing the video part
To convert the audio from stereo to mono without changing the video part, you can use FFmpeg: ffmpeg ...
- 使用ajax实现无刷新改变页面内容
如何使用ajax实现无刷新改变页面内容(也就是ajax异步请求刷新页面),下面通过一个小demo说明一下,前端页面代码如下所示 1 <%@ Page Language="C#" ...
- 在浏览器地址栏按回车、F5、Ctrl+F5刷新网页的区别
不少同学问,不都是刷新吗?还有什么区别?其实,还是有的. 其中,在地址栏按回车又分为两种情况.一是请求的URI在浏览器缓存中未过期,此时,使用Firefox的firebug插件在浏览器里显示的HTTP ...
- JS产生随机一注彩票
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8&qu ...
- WCF use ProtoBuf
ProtoBuf, 比起xml和json, 传输的数据里面没有自描述标签, 而且是基于二进制的, 所以有着超高的传输效率, 据牛人张善友的描述, 可以替代WCF的自带的编码方案, 效率有极大的提升. ...
- js常用方法
若未声明,则都是js的方法 1.indexOf indexOf(str):默认返回字符串中第一次出现索引位置 的下标,没有则返回-1 indexOf(str,position):返回从position ...
- Xocde4与Xcode3的模板比较
XCode 4.2.1 项目的模版截图: Single View Application This template provides a starting point for an applicat ...