网络编程概述
计算机网络
是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
网络编程
就是用来实现网络互连的不同计算机上运行的程序间可以进行数据交换。
网络编程三要素
IP
每个设备在网络中的唯一标识
每台网络终端在网络中都有一个独立的地址,我们在网络中传输数据就是使用这个地址。
打开控制台,你可使用 ipconfig:查看本机IP ;还可使用ping命令来测试连接(ping www.baidu.com)
本地回路地址:127.0.0.1 (可留做本机网卡测试的,还有假设你要做一些网络编程的练习,当没有网时你就可以用这个地址,自己给自己发数据)
广播地址:255.255.255.255(顾名思义是对网路上所有的ip地址进行广播自己的地址信息,4个255可对整个互联网的广播)
IPv4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽。
IPv6:8组,每组4个16进制数(可简写)。
1a2b:0000:aaaa:0000:0000:0000:aabb:1f2f
//中间为0可省略,但只能省略一处(看下面)
1a2b::aaaa:0000:0000:0000:aabb:1f2f
1a2b:0000:aaaa::aabb:1f2f
1a2b:0000:aaaa::0000:aabb:1f2f
1a2b:0000:aaaa:0000::aabb:1f2f
端口号
每个程序在设备上的唯一标识
每个网络程序都需要绑定一个端口号,传输数据的时候除了确定发到哪台机器上,还要明确发到哪个程序。
端口号范围从0-65535
编写网络应用就需要绑定一个端口号,尽量使用1024以上的,1024以下的基本上都被系统程序占用了。
常用端口
mysql: 3306
oracle: 1521
web: 80
tomcat: 8080
QQ: 4000
feiQ: 2425
协议
为计算机网络中进行数据交换而建立的规则、标准或约定的集合。
UDP
面向无连接,数据不安全,速度快。不区分客户端与服务端。
TCP
面向连接(三次握手),数据安全,速度略低。分为客户端和服务端。
三次握手: 第一次客户端先向服务端发起请求, 第二次服务端响应请求, 第三次传输数据(你瞅啥?我瞅你咋滴。来咱两干一架)
Socket通信
Socket套接字概述:
网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。
通信的两端都有Socket。网络通信其实就是Socket间的通信。数据在两个Socket间通过IO流传输。
Socket在应用程序中创建,通过一种绑定机制与驱动程序建立关系,告诉自己所对应的IP和port。
简单原理图解:
这里把我们的电脑比作港口,那么Socket相当于我们的码头,数据在两个码头(Socket)间通过船(IO流)进行运输(通信)
UDP传输
先来了解UDP下Java用什么来表示我们的码头
//用来发送和接收数据报包的套接字
public class DatagramSocket implements java.io.Closeable {
//构造套接字并将其绑定到本地主机上任何可用的端口
public DatagramSocket() throws SocketException
//构造套接字并将其绑定到本地主机上的指定端口
public DatagramSocket(int port) throws SocketException
//在该套接字上发送数据报包
public void send(DatagramPacket p) throws IOException
//在该套接字上接收数据报包
//此方法在接收到数据报前一直阻塞(简单说在没有接收到数据前会一直停在那里)
public void receive(DatagramPacket p) throws IOException
//关闭该套接字
public void close()
}
在上面出现了数据报包,这是什么?简单的说,相当于我们的集装箱,我们需要把数据包装在集装箱内,我们需要发送(Send)货物(DatagramPacket)
//相当于我们的集装箱
public final class DatagramPacket {
//构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号上
public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
//构造数据报包,用来接收长度为 length 的数据包
public DatagramPacket(byte[] buf, int length)
}
发送Send
创建DatagramSocket, 随机端口号
创建DatagramPacket, 指定数据, 长度, 地址, 端口
使用DatagramSocket发送DatagramPacket
关闭DatagramSocket
public class Send {
public static void main(String[] args) throws IOException {
String str = "注意,这不是演习,这不是演习!";
DatagramSocket socket = new DatagramSocket();//创建Socket相当于创建码头
//创建Packet相当于集装箱
DatagramPacket packet = new DatagramPacket(str.getBytes(), str.getBytes().length,InetAddress.getByName("127.0.0.1"), 8585);
socket.send(packet);//发货,将数据发出去
socket.close();//关闭码头
}
}
接收Receive
创建DatagramSocket, 指定端口号
创建DatagramPacket, 指定数组, 长度
使用DatagramSocket接收DatagramPacket
关闭DatagramSocket
从DatagramPacket中获取数据
public class Receive {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(8585);//创建Socket相当于创建码头
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);//创建Packet相当于创建集装箱
socket.receive(packet);//接货,接收数据
byte[] arr = packet.getData();//获取数据
int len = packet.getLength();//获取有效的字节个数
System.out.println(new String(arr,0,len));
socket.close();
}
}
这里为什么启动接收再去启动发送应该比较好理解的吧。因为等发送完再去接收的时候可能就来不及了(数据丢失)
来讲讲Send这个类里的DatagramSocket,这里你想象一下发货,我们只需在任意空闲的码头发货就ok,所以这里不需指定端口号。而集装箱(DatagramPacket)却是要指明了ip和端口号,这里好比集装箱的订单一样,不写明送哪里去,那怎么送货
那么Receive就比较好理解了,因为对方送货时已经确定了端口号,那么我们这里必须和Send里的DatagramPacket里的端口号一致,否则你将接不到货。那么Receive里的DatagramPacket就指明了一个字节数组和个数,这里便相当于把接收的集装箱里面的货物转到了自己的集装箱里面。
UDP传输优化和多线程
优化啥?上面我们的程序只能发送一句,现在我们改进我们的代码,实现想发几句就发几句。
为啥需要多线程呢?回顾一下,我们之前的程序是先接收在发送,正常情况下,我们的接受和发送是需要并行跑的(两个线程),还有下面代码中接收端是没有关闭的,这样可以确保一直接收到数据
public class MoreThread {
public static void main(String[] args) {
//启动两条线程,为了确保接收端先启动,你可以在两段代码中间小睡一会(这里未演示)
new Receive().start();
new Send().start();
}
}
class Receive extends Thread {
@Override
public void run() {
try {
DatagramSocket socket = new DatagramSocket(5858);//创建码头
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);//创建集装箱
while(true) {//接收端一直开着
socket.receive(packet);//接货
byte[] arr = packet.getData();//获取数据
int len = packet.getLength();//获取有效的字节个数
String ip = packet.getAddress().getHostAddress();//获取ip地址
int port = packet.getPort();//获取端口号
System.out.println(ip + ":" + port + ":" + new String(arr,0,len));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Send extends Thread {
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
DatagramSocket socket = new DatagramSocket();//创建码头
while(true) {
String line = scanner.nextLine();
if("quit".equals(line.trim())) {//当输入quit时退出
break;
}//创建集装箱
DatagramPacket packet = new DatagramPacket(line.getBytes(), line.getBytes().length,InetAddress.getByName("127.0.0.1"),5858);
socket.send(packet);//发货,将数据发出去
}
socket.close();//发送端需关闭
scanner.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
当输入quit时便不再向接收端发送数据了,而程序也未停止,因为这里接收端那条线程是没有结束(也不会结束),这样可以确保只要我们发送数据,接收端就会接收到。
TCP协议
首先,上面的UDP是不区分客户端和服务端的,所以有发和收就ok,一般用于传输一些不重要的数据,比如我们的看视频,丢个几字节可能没有太大影响。UDP的传输使用了类似 码头 和 集装箱 这样的手段来传输数据。
现在,我们来看看TCP协议。TCP是区分客户端和服务端的,牢记上面的三次握手
//该类实现客户端套接字(相当于客户端码头)
public class Socket implements java.io.Closeable {
//创建一个套接字并将其连接到指定 IP 地址的指定端口号
public Socket(InetAddress address, int port) throws IOException
//返回此套接字的输入流
public InputStream getInputStream() throws IOException
//返回此套接字的输出流
public OutputStream getOutputStream() throws IOException
//关闭此套接字
public void close()
}
//该类实现服务器套接字(服务端码头)
public class ServerSocket implements java.io.Closeable {
//创建套接字并将其绑定到本地主机上的指定端口
public DatagramSocket(int port) throws SocketException
//接收客户请求得到客户端的Socket
//此方法在连接传入之前一直阻塞(简单说当没有客户请求时该方法会一直停在那里等待请求)
public Socket accept() throws IOException
//关闭此套接字
public void close()
}
客户端
创建Socket连接服务端(指定ip地址,端口号)通过ip地址找对应的服务器
调用Socket的getInputStream()和getOutputStream()方法获取和服务端相连的IO流
输入流可以读取服务端输出流写出的数据
输出流可以写出数据到服务端的输入流
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",5858);
InputStream is = socket.getInputStream(); //获取客户端输入流
OutputStream os = socket.getOutputStream(); //获取客户端的输出流
byte[] arr = new byte[1024];
int len = is.read(arr); //读取服务器发过来的数据
System.out.println(new String(arr,0,len)); //将数据转换成字符串并打印
os.write("挖掘机技术哪家强?".getBytes()); //客户端向服务器写数据
socket.close();
}
}
服务端
创建ServerSocket(需要指定端口号)
调用ServerSocket的accept()方法接收一个客户端请求,得到一个Socket
调用Socket的getInputStream()和getOutputStream()方法获取和客户端相连的IO流
输入流可以读取客户端输出流写出的数据
输出流可以写出数据到客户端的输入流
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(5858);
Socket socket = server.accept(); //接受客户端的请求
InputStream is = socket.getInputStream(); //获取客户端输入流
OutputStream os = socket.getOutputStream(); //获取客户端的输出流
os.write("百度一下你就知道".getBytes()); //服务器向客户端写出数据
byte[] arr = new byte[1024];
int len = is.read(arr); //读取客户端发过来的数据
System.out.println(new String(arr,0,len)); //将数据转换成字符串并打印
socket.close();
server.close();//服务器一般不用关闭
}
}
这里,必须先启动服务端,想想上面的三次握手,客户端必须先和服务端打招呼,如果你先启动客户端,服务端后启动,你将会得到一个 java.net.ConnectException: Connection refused: connect 错误。上面的UDP就没事,因为他使用了集装箱进行传输,我可以确保这个货物发出去,接收就是别人的事了;而这里相当于两个码头必须先通过话(我要发货了,那你发吧),这样货物的安全得到了保证,然后还须明确指定航线(InputStream,OutputStream)进行传输货物。
在同时编写客户端和服务端的同时,你可能对输入输出流产生了疑问。这里你可以这样,当你编写服务端时,忘记客户端的存在,你要响应肯定用输出流,接收信息肯定用输入流。客户端也是一样。
TCP的优化和多线程
优化啥?可以发现上述的IO流是对字节进行操作的,所以所有的字符串都需通过转化为字节数组进行操作,那么我们可以使用BufferedReader进行输入流包装,而输出流可使用PrintStream(既可以写字节,也可以写字符串)
多线程的话,我们的服务器都是多线程的,服务器不是多线程,那么我们每次只能有一个人和服务器进行交互,那么在服务器每接收到一个请求都需开启一个线程来跑。同样的服务器是不关闭的。
客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",5858);
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream())); //将字节流包装成了字符流
PrintStream ps = new PrintStream(socket.getOutputStream());//PrintStream中有写出换行的方法
ps.println("盯~盯~!");
System.out.println(br.readLine());
ps.println("我就瞅你咋滴");
System.out.println(br.readLine());
socket.close();
}
}
服务端
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(5858); //server不关闭
while(true) {
final Socket socket = server.accept(); //接受客户端的请求
new Thread() {
public void run() {
try {
BufferedReader br = new BufferedReader(//这里的输入输出和客户端一样
new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
System.out.println(br.readLine());
ps.println("你瞅啥呢?");
System.out.println(br.readLine());
ps.println("对不起,服务器忙碌中~");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
网络编程小习
小习一:
客户端向服务器写字符串(键盘录入),服务器(多线程)将字符串反转后写回,客户端再次读取到是反转后的字符串
客户端:
public class Client {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);//创建键盘录入对象
Socket socket = new Socket("127.0.0.1", 8585);//创建客户端,指定ip地址和端口号
BufferedReader br = new BufferedReader(//获取输入输出流
new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(scanner.nextLine());//将字符串写到服务器去
System.out.println(br.readLine());//将反转后的结果读出来
socket.close();
scanner.close();
}
}
服务端:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8585);
System.out.println("服务器启动,绑定8585端口");
while(true) {
final Socket socket = server.accept(); //接受客户端的请求
new Thread() {//开启一条线程
public void run() {
try {
BufferedReader br = new BufferedReader(//获得输入输出流
new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
String line = br.readLine();//将客户端写过来的数据读取出来
line = new StringBuilder(line).reverse().toString();
ps.println(line);//反转后写回去
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
小习二:
客户端向服务器上传文件(根据文件名判断若服务器上已经存在则不上传,否则上传)
客户端
public class Client {
public static void main(String[] args) throws IOException {
File file = getFile();//获取 File
Socket socket = new Socket("127.0.0.1", 8585);
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(file.getName());//发送文件名到服务端
String result = br.readLine();//服务端返回信息判断是否已存在
while("存在".equals(result)) {
System.out.println("您上传的文件已经存在,无需重复上传");
socket.close();
return;
}//不存在则上传
FileInputStream fis = new FileInputStream(file);
byte[] arr = new byte[1024*8];
int len;
while((len = fis.read(arr)) != -1) {
ps.write(arr, 0, len);
}
fis.close();
socket.close();
}
private static File getFile() {
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个文件路径");
while(true) {
String line = sc.nextLine();
File file = new File(line);
if(!file.exists()) {
System.out.println("您录入的文件路径不存在,请重新录入:");
} else if(file.isDirectory()){
System.out.println("您录入的是文件夹路径,请输入一个文件路径:");
} else {
sc.close();
return file;
}
}
}
}
服务端
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8585);
System.out.println("服务器启动,绑定8585端口号");
while(true) {
final Socket socket = server.accept();//接受请求
new Thread() {
public void run() {
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(is));
PrintStream ps = new PrintStream(socket.getOutputStream());
String fileName = br.readLine();//获取客户端需上传的文件名信息
File dir = new File("update");
dir.mkdir();
File file = new File(dir,fileName);
//判断文件是否存在, 将结果发回客户端
if(file.exists()) {
ps.println("存在");
socket.close();
return;
} else {
ps.println("不存在");
}
//从网络读取数据, 存储到本地
FileOutputStream fos = new FileOutputStream(file);
byte[] arr = new byte[1024*8];
int len;
while((len = is.read(arr)) != -1) {
fos.write(arr, 0, len);
}
fos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}