网络编程(一)
一、网络模型
1.OSI参考模型OSI网络模型
的每一层,都有自己特有的数据封装特征信息。两台主机通信,首先在应用层将数据封装。将会给数据加上应用层的特征信息,传递给OSI网络模型的下一层表示层;表示层也会给数据加上自己特有的特征;以此类推。到传输层(TCP/IP
和UDP协议
所在层),加上TCP
的特征信息,交由网络层;网络层将IP协议
赋给该数据包,标识该数据包的目的地,再交给数据链路层;数据链路层标识该数据包的传输方式,交给最后一层物理层(网线,光纤,无线设备);这时,数据封包结束,通过物理层传输到目的主机的物理层,由对方主机,从底层物理层开始,对数据进行数据拆包。
2.TCP/IP参考模型
TCP/IP参考模型分为四个层次。它将OSI参考模型的应用层、表示层和会话层合并为应用层,保留了传输层和网际层,最后将数据链路层和物理层合并为主机至网络层。
注意:
把用户应用程序最为最高层,把物理通信线路最为最底层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。网络模型的每个层次都有自己的传输协议。传输层的协议是TCP和UDP协议;网际层最常用的是IP协议;应用层通常使用HTTP/FTP协议
二、网络通信三要素
1.对方IP地址
网络通信的首要素是要知道对方的IP地址,确定对方的主机在网络上的位置,才能发起进一步的请求。
2.应用端口
网络通信第二要素是获取对方的端口。要将数据要发送到指定的应用程序上,就必须直到对方用于接收本方数据的应用服务端口。每个网络应用程序都有一个网络端口用于接收数据。主机上的端口可以在0-65535
之间任取,但是0-1024
默认作为系统端口,不建议使用。
3.通信协议
通信协议,即通信规则。国际组织定义了一个在传输层和网络层通用的协议————TCP/IP协议
。
三、IP地址
1.概述IP(Internet Protocol)
协议是网际层的主要协议,支持网间互连的数据包通信。它提供无连接数据包传送,数据包路由选择和差错控制。IP地址分4段
,每段为1个字节
,每段最大值为255
。IP地址
用于确定主机在网络中的位置。
2、本地回环地址127.0.0.1
是本地回环地址,可以用于测试网卡工作是否正常或这用于测试本地网页等。
3.局域网IP地址192.168
开头的IP被保留,不用于公网,用于局域网。其中有一个例外,第四个字段为255
的被保留作广播IP地址
,给这个IP发送的消息,局域网内所有主机都能接收。
4.子网掩码
子网掩码用于划分局域网。由于主机数量越来越多,出现了IP不够的情况。划分子网,让多台主机使用同一个公网IP,部分解决了这一问题。
5.域名
IP地址有4段,不易记忆,通常给主机起一个名字,如127.0.0.1
称为localhost
,这就是127.0.0.1这个IP地址的域名
。
四、通信协议
1.UDP协议
1)概述UDP(user data protocol)
向应用程序提供了一种发送封装的原始IP数据包的方法。该协议面向无连接,通信主机一端只负责发送数据,不负责对方是否接收到数据;如果对方处于在线状态,就能接收到数据包;如果不在线,该数据包就丢失。UDP
在传输数据时,两通信主机不一定已经建立连接。
2)特点
1> 将数据、数据源和目的封装成数据包;
2> 数据包大小有限制,限制在64k
以内;
3> 不需要建立连接,协议不可靠
4> 速度快,效率高
2.TCP协议
1)概述TCP(transmission control protocol)
是专门设计用于在不可靠的因特网上提供可靠的、端到端的字节流通讯协议。它是一种面向连接的协议。TCP
与UDP
协议相反,是面向连接的协议。该协议在传输数据前必须先建立连接。这个连接的建立是基于三次握手的机制之上的。三次握手确保了数据连接通路的存在,一旦通信双方有一方断开连接,数据传输即刻停止。
2)特点
1> 数据传输大小无限制
2> 通过三次握手机制确定连接通路畅通
3> 必须建立连接,协议可靠
4> 速度较慢,效率较低
五、网络编程常用类
1.Socket类
1)概述
网络编程其实就是Socket编程
。Socket
是为网络服务提供的一种机制,是通信得以进行的前提。通信两端的主机都有Socket
,两端通信其实就是Socket间在通信,数据在两个Socket
间通过IO
进行传输。TCP
和UDP
协议,都是基于Socket
进行数据传输。
2)不同协议的Socket创建
每个传输协议都有自己建立传输端点的方式。下面进行不同协议的Socket服务
的建立。
- UDP协议建立发送端Socket
DatagramSocket和DatagramPacket类DatagramSocket
类表示用来发送和接收数据包的套接字(Socket
)。该类既能发送,又能接收。DatagramSocket
在发送数据时,提供了数据包对象DatagramPacket
,用于数据封包。
第一步:
建立发送端UDPSocket服务
/* 通过DatagramSocket对象创建UDP Socket服务 */
DatagramSocket ds = new DatagramSocket();
第二步:
提供数据,并将数据封装到数据包中
/* 通过DatagramPacket对象创建数据包对象 */
byte[] buf = "UDP ge men er lai la !~~#!@$@%#^$&".getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("localhost"), 7777);
第三步:
通过Socket服务发送数据包
/* 调用send()方法,通过Socket服务发送数据包 */
ds.send(dp);
第四步:
关闭资源
/* 关闭资源 */
ds.close();
- UDP协议建立接收端Socket
第一步:
建立接收端UDPSocket服务
/* 通过DatagramSocket建立接收端Socket服务,并监听相应端口*/
/* 定义监听端口很重要,发送端会将数据发送到7777端口,如果补监听,收不到数据 */
DatagramSocket ds = new DatagramSocket(7777);
第二步:
定义数据包对象
/* 创建数据包对象,用于存储数据 */
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
第三步:
通过Socket服务接收数据包
/* 调用reveive()方法接收数据包 */
ds.receive(dp);
第四步:
解析数据包
/* 解析数据,获取地址,数据,长度等*/
String ip = dp.getAddress().getHostAddress();
String data = new String(dp.getData(), 0, dp.getLength());
int port = dp.getPort();
System.out.println(ip + "::" + data + "::" + port);
第五步:
关闭资源
/* 关闭资源 */
ds.close();
程序运行结果:127.0.0.1::UDP ge men er lai la !~~#!@$@%#^$&::64979
注意:
UDP接收端的receive()方法是阻塞式方法完整示例代码:
package com.heisejiuhuche.socket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class DatagramSendSocketDemo {
public static void main(String[] args) throws IOException {
/* 通过DatagramSocket对象创建UDP Socket服务 */
DatagramSocket ds = new DatagramSocket();
/* 通过DatagramPacket对象创建数据包对象 */
byte[] buf = "UDP ge men er lai la !~~#!@$@%#^$&".getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("localhost"), 7777);
/* 调用send()方法,通过Socket服务发送数据包 */
ds.send(dp);
/* 关闭资源 */
ds.close();
}
}
package com.heisejiuhuche.socket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class DatagramRecvSocketDemo {
public static void main(String[] args) throws IOException {
/* 通过DatagramSocket建立接收端Socket服务,并监听相应端口*/
DatagramSocket ds = new DatagramSocket(7777);
/* 创建数据包对象,用于存储数据 */
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
/* 调用reveive()方法接收数据包 */
ds.receive(dp);
/* 解析数据,获取地址,数据,长度等*/
String ip = dp.getAddress().getHostAddress();
String data = new String(dp.getData(), 0, dp.getLength());
int port = dp.getPort();
System.out.println(ip + "::" + data + "::" + port);
/* 关闭资源 */
ds.close();
}
}
练习1:
键盘录入方式发送数据
示例代码:
package com.heisejiuhuche.socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class SendFromKeyDemo {
public static void main(String[] args) throws Exception {
/* 创建DatagramSocket对象 */
DatagramSocket ds = new DatagramSocket();
/* 创建缓冲流对象,接收键盘录入 */
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
String line = null;
/* 接收录入,如果录入over,结束程序;如果不是,将录入的内容发送出去 */
while((line = bufr.readLine()) != null) {
if(line.equals("886"))
break;
byte[] buf = line.getBytes();
DatagramPacket dp = new DatagramPacket(
buf, buf.length, InetAddress.getByName("localhost"), 10000);
ds.send(dp);
}
/* 关闭资源 */
ds.close();
}
}
package com.heisejiuhuche.socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ConstantRecvDemo {
public static void main(String[] args) throws Exception {
/* 创建DatagramPacket对象用于接收数据,并监听相应端口 */
DatagramSocket ds = new DatagramSocket(10000);
/* 创建缓冲区数组 */
byte[] buf = new byte[1024];
/* 调用receive方法获取数据并解析 */
while(true) {
DatagramPacket dp = new DatagramPacket(buf, buf.length);
ds.receive(dp);
String IP = dp.getAddress().getHostAddress();
String data = new String(dp.getData(), 0, dp.getLength());
System.out.println(IP + ":" + data);
}
}
}
练习2:
使用多线程技术,编写聊天小程序。
示例代码:
package com.heisejiuhuche.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.BindException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class ChatDemo {
public static void main(String[] args) {
try {
/* 启动发送和接收线程 */
DatagramSocket dsSend = new DatagramSocket();
DatagramSocket dsRecv = new DatagramSocket(10002);
new Thread(new Client(dsSend)).start();
new Thread(new Server(dsRecv)).start();
} catch(BindException e) {
throw new RuntimeException("Port in use...");
} catch(SocketException e) {
throw new RuntimeException("Socket create failed....");
}
}
}
class Client implements Runnable {
private DatagramSocket ds;
Client(DatagramSocket ds) {
this.ds = ds;
}
public void run() {
/* 不断从键盘读取数据并发送 */
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
String line = null;
try {
while((line = bufr.readLine()) != null) {
if(line.equals("over"))
break;
byte[] buf = line.getBytes();
DatagramPacket dp = new DatagramPacket(
buf, buf.length, InetAddress.getByName("localhost"), 10002);
ds.send(dp);
}
} catch(UnknownHostException e) {
throw new RuntimeException("Unknown host...");
} catch(IOException e) {
throw new RuntimeException("Read failed...");
} finally {
try {
if(bufr != null)
bufr.close();
} catch(IOException e) {
throw new RuntimeException("Bufr shutdown failed...");
}
}
}
}
class Server implements Runnable {
private DatagramSocket ds;
Server(DatagramSocket ds) {
this.ds = ds;
}
/* 不断接收数据并打印在控制台 */
public void run() {
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
try {
while(true) {
ds.receive(dp);
String ip = dp.getAddress().getHostAddress();
String data = new String(dp.getData(), 0, dp.getLength());
System.out.println(ip + ": " + data);
}
} catch(IOException e) {
throw new RuntimeException("Receive failed...");
}
}
}
这是一个程序同时实现发送和接收的任务,一个线程负责发送,一个线程负责接收。
- 客户端建立TCP Socket服务
TCP
协议通讯分为客户端和服务端。客户端对应Socket
类,服务端对应ServerSocket
类。创建客户端对象时,就可以指定连接主机的地址和端口,形成数据通路。
第一步:
创建Socket服务并指定主机和端口
/* 创建客户端的Socket服务,指定目标主机和端口 */
Socket s = new Socket("localhost", 10002);
注意:
通路一旦建立,就存在一个Socket流
;该流中既有输入流,也有输出流,可以使用getInputStream()
方法和getOutputStream()
方法获取输入流和输出流
第二步:
获取输出流,发送数据
/* 调用getOutputStream()方法,获取输出流,发送数据 */
OutputStream out = s.getOutputStream();
out.write("TCP data coming...".getBytes());
第三步:
关闭资源
/* 关闭Socket资源 */
s.close();
- 服务端建立Socket服务
服务端原理:
客户端和服务端建立数据通路后,服务端会拿到该客户端的对象,使用该对象的输入和输出流与该对象进行通信。这样可以避免通信干扰,保证数据能被正确的客户接收。
第一步:
建立服务端Socket服务
/* 通过ServerSocket建立服务端Socket服务,绑定端口 */
ServerSocket ss = new ServerSocket(10002);
第二步:
获取连接的客户端对象
/* 调用accept()阻塞式方法获取连接对象 */
Socket s = ss.accept();
第三步:
服务端使用相应的客户端输入流读取数据
/* 获取客户端输入流对象读取数据 */
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
String ip = s.getInetAddress().getHostAddress();
System.out.println(new String(buf, 0, len));
System.out.println(ip + "connected...");
第四步:
关闭资源
/* 关闭客户端服务,释放服务端资源 */
s.close();
/* 可选操作,如持续服务,无须关闭 */
ss.close();
完整示例代码:
package com.heisejiuhuche.socket;
/* 客户端 */
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws Exception {
Socket s = new Socket("localhost", 10003);
OutputStream out = s.getOutputStream();
out.write("TCP data coming...".getBytes());
s.close();
}
}
package com.heisejiuhuche.socket;
/* 服务端 */
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10003);
Socket s = ss.accept();
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip + "connected... " + new String(buf, 0, len));
s.close();
ss.close();
}
}
程序运行结果:127.0.0.1connected... TCP data coming...
- 客户端服务端互访
客户端
第一步:
建立Socket服务,指定主机和端口
Socket s = new Socket("localhost", 10004);
第二步:
获取输出流,发送数据
OutputStream out = s.getOutputStream();
out.write("服务端你好,这里是客户端~".getBytes[]);
第三步:
获取输入流,接收服务端反馈信息
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
第四步:
关闭客户端资源
s.close();
服务端
第一步:
建立Socket服务,监听端口
ServerSocket ss = new ServerSocket(10004);
第二步:
获取客户端输入流对象,读取数据
Socket s = ss.accpet();
/* 获取连接的主机IP地址并打印 */
System.out.println(s.getInetAddress().getHostAddress());
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
第三步:
获取客户端输出流对象,发送数据
OutputStream out = s.getOutputStream();
out.write("哥们儿收到,你也好~".getBytes());
第四步:
关闭资源
s.close();
ss.close();
完整代码:
package com.heisejiuhuche.socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws Exception {
Socket s = new Socket("localhost", 10003);
OutputStream out = s.getOutputStream();
out.write("哥们儿你好~".getBytes());
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
s.close();
}
}
package com.heisejiuhuche.socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10003);
Socket s = ss.accept();
System.out.println(s.getInetAddress().getHostAddress() + "connected... ");
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
OutputStream out = s.getOutputStream();
out.write("哥们儿收到,你也好~".getBytes());
s.close();
ss.close();
}
}
程序运行结果:
127.0.0.1connected...
哥们儿你好~
哥们儿收到,你也好~
练习1:
要求从客户端写入文本数据,服务端接收到数据后转成大写发送回给客户端。
示例代码:
package com.heisejiuhuche.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
/* 客户端代码 */
public class TransTextClient {
public static void main(String[] args) throws Exception {
/* 创建Socket对象,指定主机和端口 */
Socket s = new Socket("localhost", 10005);
/* 建立键盘录入 */
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
/* 创建Socket输出流对象 */
BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
/* 创建Socket输入流对象 */
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
/* 往服务器端发送数据 */
String line = null;
while((line = bufr.readLine()) != null) {
if(line.equals("over"))
break;
bufOut.write(line);
String upper = bufIn.readLine();
System.out.println("Server: " + upper);
}
/* 关闭资源 */
bufr.close();
s.close();
}
}
package com.heisejiuhuche.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
/* 服务端代码 */
public class TransTextServer {
public static void main(String[] args) throws Exception {
/* 创建ServerSocket对象 */
ServerSocket ss = new ServerSocket(10005);
/* 拿到Socket对象 */
Socket s = ss.accept();
/* 打印客户端IP地址 */
System.out.println(s.getInetAddress().getHostAddress());
/* 创建Socket输入流,接收数据 */
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
/* 创建Socket输出流,发送数据 */
BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
/* 接收客户端数据,转换成大写再发送会客户端 */
String line = null;
while((line = bufIn.readLine()) != null) {
bufOut.write(line.toUpperCase());
}
/* 关闭资源 */
s.close();
ss.close();
}
}
问题:
服务端收不到信息,客户端无法继续输入。
原因:
-使用缓冲字符输出流,数据写入缓冲区,应该调用flush()
方法刷新;
-客户端和服务端都有阻塞式方法,这些方法没有读到结束标记,就会一直等待,应该调用newLine()
方法换行
修改两个while()循环:
/* 客户端while */
while((line = bufr.readLine()) != null) {
if(line.equals("over"))
break;
bufOut.write(line);
/* 加入换行,使readLine()方法能读到换行符并返回数据 */
bufOut.newLine();
/* 刷新缓冲区 */
bufOut.flush();
String upper = bufIn.readLine();
System.out.println("Server: " + upper);
}
/* 服务器端while */
while((line = bufIn.readLine()) != null) {
bufOut.write(line.toUpperCase());
bufOut.newLine();
bufOut.flush();
}
至此,程序运行正常。
使用PrintWriter
,设置自动刷新缓冲区,只需在两个while循环
中调用println()
方法,既能刷新缓冲区,又能有换行符号。
简化代码如下:
PrintWriter pw = new PrintWriter(s.getOutputStream(), true);
pw.println(line);
注意:
-readLine()方法必须读到回车符才会返回数据;
-输入over的时候,服务端也停止运行;因为客户端的关闭资源的时候,会往Socket流中发送-1;而服务器端调用的readLine()方法,底层的read()读到了这个-1,也会返回一个-1;那么服务器端的循环就结束了
练习2:
上传文件,保存在服务器端。
示例代码:
package com.heisejiuhuche.socket;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class UploadFileClient {
public static void main(String[] args) throws Exception {
Socket s = new Socket("localhost", 10006);
/* 创建file对象,关联要上传的文件 */
File file = new File("C:\\Users\\jeremy\\Documents\\javaTmp\\Send.java");
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
/* 创建缓冲字符输入流,用于读取文件 */
BufferedReader bufr = new BufferedReader(new FileReader(file));
PrintWriter pw = new PrintWriter(s.getOutputStream(), true);
/* 现将要上传的文件名通知服务器端 */
pw.println(file.getName());
String line = null;
while((line = bufr.readLine()) != null) {
pw.println(line);
}
s.shutdownOutput();
System.out.println(bufIn.readLine());
/* 发送文件上传结束标记给服务器端,以便服务器端停止读取 */
bufr.close();
s.close();
}
}
package com.heisejiuhuche.socket;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public interface UploadFileServer {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(10006);
Socket s = ss.accept();
System.out.println(s.getInetAddress().getHostAddress());
BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
/* 接收文件名 */
File path = new File("C:\\Users\\jeremy\\Documents\\");
File file = new File(path, bufIn.readLine());
/* 如果文件不存在,创建文件 */
if(!file.exists()) {
file.createNewFile();
}
PrintWriter pw = new PrintWriter(new FileWriter(file), true);
String line = null;
while((line = bufIn.readLine()) != null) {
pw.println(line);
}
PrintWriter pw2 = new PrintWriter(s.getOutputStream(), true);
pw2.println("上传成功");
pw.close();
s.close();
ss.close();
}
}
程序运行结果:上传成功
注意:
上传的时候,首先最好把文件名传给服务器端,让服务器端建立文件来储存数据;如果重名,要进行重命名