在计网的基础之上,设备之间的通信是通过网络实现数据传输,将数据通过网络从一台设备传输到另一台设备。在前面的学习中我们知道java.lang包提供基础类库、java.io包中提供io功能的函数,而java.net包中则提供了用于网络连接的类或接口来让我们实现网络通信,其实Java网络编程的本质还是面向接口和类编程
局域网:覆盖范围最小,仅仅覆盖一个教室或一个机房
城域网:覆盖范围较大,可以覆盖一个城市
广域网:覆盖范围最大,可以覆盖全国,甚至全球,万维网是广域网的代表
IP地址:网络中的设备标记,每一台主机都有唯一的ip地址,主要分为IPv4,IPv6
地址形式:公网地址、私网地址,特殊ip地址:127.0.0.1或者localhost表示本机
域名:解决记忆ip地址难的问题引出域名,比如www.baidu.com
端口号:标识计算机上特定的网络程序,被规定为一个16位的二进制,范围是0~65535,比如tomcat——8080、mysql——3306、sqlserver——1433…
端口类型:周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
注册端口:1024~49151,分配给用户进程或某些应用程序。(如:Tomcat占用8080,MySQL占用3306)
动态端口:49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序选择注册端口时,同一个设备不能出现两个端口号一样的程序,否则报错
网络通信模型:
1.OSI模型自顶向下:应用层-表示层-会话层-传输层-网络层-数据链路层-物理层
2.TCP/IP模型自顶向下:应用层-传输层(TCP)-网络层(IP)-物理+数据链路层
协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议。
TCP协议:
使用TCP协议前,须先建立TCP连接,它是一种面向连接的可靠通信协议,形成传输数据通道传输前,采用“三次握手“方式,是可靠的,TCP协议进行通信的两个应用进程:客户端、服务端在连接中可进行大数据量的传输,传输完毕,需释放已建立的连接,效率低
TCP协议通信:
UDP协议:
用户数据报协议,将数据、源、目的封装成数据包,不需要建立连接,每个数据报的大小限制在64K内,不适合传输大量数据,因无需连接,故是不可靠的,发送数据结束时无需释放资源(因为不是面向连接的),速度快,效率高,但不安全,容易丢失数据
套接字Socket:
套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准。通信的两端都要有Socket,是两台机器间通信的端点,网络通信其实就是Socket间的通信,Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。一般主动发起通信的应用程序属客户端,等待通信请求的为服务端
前面说过,网络编程的本质还是面向接口和类编程,Java的核心类库向我们提供了面向通信协议和网络模型编程的通道,这是面向对象思想的一种体现
所以,我们通过API连接端口,面向协议就可以实现网络编程
InetAddress类用于表示Internet协议(IP)地址,是序列化接口Serializable的实现类,同时也是Inet4Address、Inet6Address的父类
名称 | 说明 |
---|---|
public static InetAddress getLocalHost() | 返回本主机的地址对象 |
public static InetAddress getByName(String host) | 得到指定主机的IP地址对象,参数是域名或者IP地址 |
public String getHostName() | 获取此IP地址的主机名 |
public String getHostAddress () | 返回IP地址字符串 |
public boolean isReachable(int timeout) | 在指定毫秒内连通该IP地址对应的主机连通返回true |
代码演示:
// 1.获取本机地址对象。
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());
// 2.获取域名ip对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
// 3.获取公网IP对象。
InetAddress ip3 = InetAddress.getByName("112.80.248.76");
System.out.println(ip3.getHostName());
System.out.println(ip3.getHostAddress());
主要用于创建发送端数据包对象
构造器:
public DatagramPacket(byte[] buf,int length,InetAddness address,int port)
buf | 要发送的内容,字节数组 |
---|---|
length | 要发送内容的字节长度 |
address | 接收端的IP地址对象 |
port | 要发送的内容,字节数组 |
成员方法:public int getLength()
来获取实际收到的字节数
主要用于发送端和接收端对象
构造器:
public DatagramSocket() | 创建发送端的Socket对象,系统会随机分配一个端口号。 |
---|---|
public DatagramSocket(int port) | 创建接收端的Socket对象并指定端口号 |
成员方法:
public void send(DatagramPacket dp)
用于发送数据包
public void receive(DatagramPacket dp)
用于接收数据包
①创建DatagramSocket对象(发送端对象)
②创建Datagramacket对象封装需要发送的数据(数据包对象)
③使用DatagramSocket对象的send方法传入DatagramPacket对象
④释放资源
// 1、创建发送端对象 发送端自带默认的端口号
DatagramSocket socket = new DatagramSocket(6666);
// 2、创建一个数据包对象封装数据
byte[] buffer = "hello java".getBytes();
DatagramPacket packet = new DatagramPacket( buffer, buffer.length,InetAddress.getLocalHost() , 8888);
// 3、发送数据出去
socket.send(packet);
// 4、关闭连接
socket.close();
服务器端:
①创建DatagramSocket对象并指定端口(接收端对象)
②创建DatagramPacket对象接收数据(数据包对象)
③使用DatagramSocket对象的receive方法传入DatagramPacket对象
④释放资源
// 1、创建接收端对象:注册端口
DatagramSocket socket = new DatagramSocket(8888);
// 2、创建一个数据包对象接收数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 3、等待接收数据。
socket.receive(packet);
// 4、取出数据即可
int len = packet.getLength();
String rs = new String(buffer,0, len);
System.out.println("收到:" + rs);
// 获取发送端的ip和端口
String ip =packet.getSocketAddress().toString();
System.out.println("对方地址:" + ip);
int port = packet.getPort();
System.out.println("对方端口:" + port);
socket.close();
客户端:
①创建DatagramSocket对象(发送端对象)
②使用while死循环不断的接收用户的数据输入,如果用户输入"back"则退出程序
③如果用户输入的不是exit,把数据封装成DatagramPacket
④使用DatagramSocket对象的send方法将数据包对象进行发送—开 ⑤释放资源
// 1、创建发送端对象:发送端自带默认的端口号
DatagramSocket socket = new DatagramSocket(7777);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = scanner.nextLine();
if("back".equals(msg)){
System.out.println("离线成功!");
socket.close();
break;
}
// 2、创建一个数据包对象封装数据
byte[] buffer = msg.getBytes();
DatagramPacket packet = new DatagramPacket( buffer, buffer.length,InetAddress.getLocalHost() , 8888);
// 3、发送数据出去
socket.send(packet);
}
服务器端:
①创建DatagramSacket对象并指定端口(接收端对象)
②创建DatagramPacket对象接收数据(数据包对象)
③使用while死循环不断的进行第4步
④使用DatagramSocket对象的receive方法传入DatagramPacket对象
// 1、创建接收端对象:注册端口
DatagramSocket socket = new DatagramSocket(8888);
// 2、创建一个数据包对象接收数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
// 3、等待接收数据
socket.receive(packet);
// 4、取出数据即可
// 读取多少倒出多少
int len = packet.getLength();
String rs = new String(buffer,0, len);
System.out.println("收到了来自:" + packet.getAddress() +", 对方端口是" +packet.getPort() +"的消息:" + rs);
}
之所以可以接收很多发送端的消息是因为服务器的接收端只负责接收数据包
扩展
UDP实现广播:发送端发送的数据包的目的地写的是广播地址,且指定端口,本机所在的网段的机群程序注册对应端口
UDP实现组播:发送端的数据包目的地是组播IP,接收端必须绑定改组播IP,端口要注册发送端的目的端口
客户端:
①创建客户端的Socket对象,请求与服务端的连接
②使用socket对象调用getQutputStream()方法得到字节输出流
③使用字节输出流完成数据的发送
④释放资源:关闭socket管道
// 1、创建Socket通信管道请求有服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2、从socket通信管道中得到一个字节输出流 负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
// 4、发送消息
ps.println("hello java");
ps.flush(); //刷新!
// 关闭资源
socket.close();
服务器端:
注册端口调用方法接收就完事儿
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
if ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "收到的内容是:: " + msg);
}
客户端:
①创建ServerSocket对象,注册服务端端口
②调用ServerSacket对象的accept()方法,等待客户端的连接,并得到Socket管道对象
③通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收
④释放资源:关闭socket管道
// 1、创建Socket通信管道请求有服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2、从socket通信管道中得到一个字节输出流 负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请发送:");
String msg = sc.nextLine();
// 4、发送消息
ps.println(msg);
ps.flush();
}
// 关闭资源。
socket.close();
服务器端:
接收请求,io读取
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
while (true) {
// 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
while ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "收到的内容是:: " + msg);
}
}
上述一发一收、多发多收的本质还是客户端发,服务器端接、客户端循环发,服务器端循环接
服务器端是单线程的,每次只能处理一个客户端的消息
设计思路:
服务器端主线程定义一个循环体用来接收客户端Socket连接,每连接到一个Socket通道后分配一个独立的线程来处理它,如图所示
1.此时的客户端还是在不断地发消息
// 1、创建Socket通信管道请求有服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2、从socket通信管道中得到一个字节输出流 负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
// 4、发送消息
ps.println(msg);
ps.flush();
}
// 关闭资源。
socket.close();
2.这时的服务器端要同时处理多个客户端的通信需求
首先把主线程写好,无限循环用来接收客户端的消息
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+ "鸡汤来咯~");
// 3、开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
3.实现Thread类中的子线程用来处理主线程随机分配下来的的任务,主线程无限的接,接到一个消息主线程就new一个子线程然后start()
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){this.socket = socket;}
@Override
public void run() {
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
while ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "收到的消息是:: " + msg);
}
}
}
实现同时处理多个客户端消息的本质,就是把创建子线程的代码写到了服务器端接收消息的死循环里
上述的多用户通信模式存在一定的问题
由于是N-N的关系,所以并发问题比较严重,这个问题就等到学完并发编程再回来优化吧
面试官:假如子网掩码维255.255.255.245有多少个ip可用?
答:
256-245-2=9个
面试官:判断192.162.1.1 是A、B、C类那种网络ip地址?
答:
C类、C类P以110开头,从192.0.0.1到223.255.255.255
面试官:TCP和UDP有什么区别?
答:
1.TCP提供面向连接的传输,通信前要先建立连接(三次握手机制);UDP提供无连接的传输,通信前不需要建立连接
2.TCP提供可靠的传输(有序,无差错,不丢失,不重复);UDP提供不可靠的传输
3.TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组;UDP是面向数据报的传输,没有分组开销
4.TCP提供拥塞控制和流量控制机制;UDP不提供拥塞控制和流量控制机制
面试官:进程间通讯的方式有哪些?
答:
1.管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程之间使用。进程的亲缘关系通常是指父子进程关系
2.有名管道(FIFO):有名管道也是半双工的通信方式,但是允许在没有亲缘关系的进程之间使用,管道是先进先出的通信方式。
3.信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
4.消息队列:消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5.信号 ( sinal ) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
6.共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC
方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
7.套接字( socket ) :套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信
面试官:tcp连接建立时三次握手的具体过程,以及每一步原因?
答:
第一步:源主机A的TCP向主机B发出连接请求报文段,其首部中的SYN(同步)标志位应置为1,表示想与目标主机B进行通信,并发送一个同步序列号X(例:SEQ=100)进行同步,表明在后面传送数据时的第一个数据字节的序号是X+1(即101)。SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号
第二步:目标主机B的TCP收到连接请求报文段后,如同意,则发回确认。在确认报中应将ACK位和SYN位置1,表示客户端的请求被接受。确认号应为X+1(图中为101),同时也为自己选择一个序号Y。
第三步:源主机A的TCP收到目标主机B的确认后要向目标主机B给出确认,其ACK置1,确认号为Y+1,而自己的序号为X+1。TCP的标准规定,SYN置1的报文段要消耗掉一个序号
——部分题目引自《常见网络编程面试题整理》作者:繁华如梦