每次面试都问我TCP、UDP、Socket、HTTP、IO、Netty、RPC等网络编程,宝宝很不开心
文章非常长!!! 非常详细!!!,此次网络编程系列分为很多篇文章,后续补齐
同系列的文章地址为:
超硬核!!!一篇文章搞定BIO、NIO、AIO、Netty(详细基础内容+网络编程内容+代码示例)【网络编程 2】
第一场:
面试官:你说一下TCP的三次握手
我:第一次Client将SYN置1…、第二次Server收…、 第三次…
面试官:很难背吧?
我:…是啊,很难,要不我在和你说说四次挥手?
面试官:别了别了回去等通知吧…
我:"…"
第二场:
心里憋了一万个草泥马来到的第二家
…
面试官:你说一下TCP的三次握手
我(心里在想,还来?
):没什么好说的,就是为了保持一次网络通信交互正常
面试官:你能说的清楚一点吗?
我:就等于是你在不认识我的情况下打我的电话让我来面试
面试官:“懵了”,好像是这么回事
面试官:你说一下TCP的四次挥手
我:等于是我在上家公司辞职了
面试官:“想了一下”,能不能说的清楚一点吗?
我:我找老板办理离职,老板说可以,老板接着给我办理离职,我才可以走
面试官:有道理!
面试官:你说一下TCP和UDP的区别吧
我:TCP等于和陌生人打电话处理事情,UDP等于发广播
面试官:“…”有道理
面试官:你期望薪资多少
我:15K
面试官:下周一有时间入职?
我:…也懵了,面试到底是要怎样面试
接着进入正题
网络编程的本质是多台计算机之间的数据交换。数据传递本身没有多大的难度,不就是把一个设备中的数据发送给其他设备,然后接受另外一个设备反馈的数据。现在的网络编程基本上都是基于请求/响应方式的,也就是一个设备发送请求数据给另外一个,然后接收另一个设备的反馈。在网络编程中,发起连接程序,也就是发送第一次请求的程序,被称作客户端(Client),等待其他程序连接的程序被称作服务器(Server)。客户端程序可以在需要的时候启动,而服务器为了能够时刻相应连接,则需要一直启动。
例如以打电话为例,首先拨号的人类似于客户端,接听电话的人必须保持电话畅通类似于服务器。连接一旦建立以后,就客户端和服务器端就可以进行数据传递了,而且两者的身份是等价的。在一些程序中,程序既有客户端功能也有服务器端功能,最常见的软件就是QQ、微信这类软件了。
网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机, 另一个就是找到主机后如何可靠高效的进行数据传输。在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机。
而TCP层则提供面向应用的可靠(TCP)的或非可靠(UDP)的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的。
目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提 出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也 能及时得到服务。
计算机网络体系结构
OSI参考模型
OSI(Open System Interconnect),即开放式系统互联。一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及,推出了OSI参考模型,这样所有的公司都按照统一的标准来指定自己的网络,就可以互通互联了。
OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)。
TCP/IP参考模型
TCP/IP四层协议(数据链路层、网络层、传输层、应用层)应用层
应用层最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,TELNET等。
传输层
建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。
网络层
本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。
数据链路层
通过一些规程或协议来控制这些数据的传输,以保证被传输数据的正确性。实现这些规程或协议的硬件和软件加到物理线路,这样就构成了数据链路,
TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成对的两个之间必须建 立连接),TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达
UDP它是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可靠性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
1.2 TCP与UDP区别:TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达;
UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;
TCP通信类似于于要打个电话,接通了,确认身份后,才开始进行通行;
UDP通信类似于学校广播,靠着广播播报直接进行通信。
TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;
TCP是面向字节流的,UDP是面向报文的;
面向字节流是指发送数据时以字节为单位,一个数据包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。
TCP首部开销(20字节)比UDP首部开销(8字节)要大
UDP 的主机不需要维持复杂的连接状态表
1.3 TCP和UDP的应用场景:对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以容忍;其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况
1.4 TCP例子:TCP通信可看作打电话:
李三(拨了个号码):喂,是王五吗?
王五:哎,您谁啊?
李三:我是李三,我想给你说点事儿,你现在方便吗?
王五:哦,我现在方便,你说吧。
甲:那我说了啊?
乙:你说吧。
(连接建立了,接下来就是说正事了…)
1.5 UDP例子:
UDP通信可看为学校里的广播:
播音室:喂喂喂!全体操场集合
在网络数据传输中,传输层协议TCP是要建立连接的可靠传输,TCP建立连接的过程,我们称为三次握手。
1.6.2 三次握手的具体细节 第一次握手:Client将SYN置1,随机产生一个初始序列号seq发送给Server,进入SYN_SENT状态; 第二次握手:Server收到Client的SYN=1之后,知道客户端请求建立连接,将自己的SYN置1,ACK置1,产生一个acknowledge number=sequence number+1,并随机产生一个自己的初始序列号,发送给客户端;进入SYN_RCVD状态; 第三次握手:客户端检查acknowledge number是否为序列号+1,ACK是否为1,检查正确之后将自己的ACK置为1,产生一个acknowledge number=服务器发的序列号+1,发送给服务器;进入ESTABLISHED状态;服务器检查ACK为1和acknowledge number为序列号+1之后,也进入ESTABLISHED状态;完成三次握手,连接建立。 1.6.3 用现实理解三次握手的具体细节三次握手的目的是建立可靠的通信信道,主要的目的就是双方确认自己与对方的发送与接收机能正常。
第一次握手:客户什么都不能确认;服务器确认了对方发送正常 第二次握手:客户确认了:自己发送、接收正常,对方发送、接收正常;服务器确认 了:自己接收正常,对方发送正常 第三次握手:客户确认了:自己发送、接收正常,对方发送、接收正常;服务器确认 了:自己发送、接收正常,对方发送接收正常在网络数据传输中,传输层协议断开连接的过程我们称为四次挥手
1.7.2 四次挥手的具体细节 第一次挥手:Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态; 第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1;进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。 第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态; 第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入CLOSED状态。完成四次挥手。 1.7.3 用现实理解三次握手的具体细节TCP的四次挥手四次挥手断开连接是因为要确定数据全部传书完了
客户与服务器交谈结束之后,客户要结束此次会话,就会对服务器说:我要关闭连接了(第一 次挥手) 服务器收到客户的消息后说:好的,你要关闭连接了。(第二次挥手) 然后服务器确定了没有话要和客户说了,服务器就会对客户说,我要关闭连接了。(第三次挥 手) 客户收到服务器要结束连接的消息后说:已收到你要关闭连接的消息。(第四次挥手),才关闭 2 Socket 1 什么是Socket网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。
但是,Socket所支持的协议种类也不光TCP/IP、UDP,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。
Socket偏向于底层。一般很少直接使用Socket来编程,框架底层使用Socket比较多,
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
3 Socket通讯的过程基于TCP:服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
基于UDP:UDP 协议是用户数据报协议的简称,也用于网络数据的传输。虽然 UDP 协议是一种不太可靠的协议,但有时在需要较快地接收数据并且可以忍受较小错误的情况下,UDP 就会表现出更大的优势。我客户端只需要发送,服务端能不能接收的到我不管
4 TCP协议Socket代码示例:先运行服务端,在运行客户端
new FileOutputStream("D:\\test.png"); //文件不要存在,这只是我用来保存文件的位置
package com.lijie;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
//TCP协议Socket:服务端
public class Server {
public static void main(String[] args) {
try {
//声明服务器的端口号
ServerSocket serverSocket = new ServerSocket(8888);
//到此会暂停程序,等待接收一个socket后才执行后续操作
System.out.println("等待TCP协议传输数据");
Socket socket = serverSocket.accept();
//根据socket获得输入流
InputStream inputStream = socket.getInputStream();
//创建输出流
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test.png");//文件不要存在,这只是我用来保存文件的位置
byte[] car = new byte[1024];
int length = 0;
//将接收的输入流写入输出流,即写入本地文件中
while ((length = inputStream.read(car)) != -1) {
fileOutputStream.write(car);
}
//刷新缓冲区
fileOutputStream.flush();
//关闭资源
fileOutputStream.close();
inputStream.close();
socket.close();
System.out.println("TCP协议Socket接受成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:new FileInputStream("D:\\img\\299406.jpg");
//文件必须存在
package com.lijie;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.Socket;
//TCP协议Socket:客户端
public class Client {
public static void main(String[] args) {
try {
//创建套接字对象socket并封装ip与port
Socket socket = new Socket("127.0.0.1", 8888);
//根据创建的socket对象获得一个输出流
OutputStream outputStream = socket.getOutputStream();
//创建输入流,即要从本地发送给服务器的文件输入流,图片要真实存在
FileInputStream fileInputStream = new FileInputStream("D:\\img\\299406.jpg");//文件必须存在
byte[] car = new byte[1024];
int length = 0;
//将本地文件以IO流的方法发送给服务器
while ((length = fileInputStream.read(car)) != -1) {
outputStream.write(car, 0, length);
}
System.out.println("TCP协议的Socket发送成功");
//刷新缓冲区
outputStream.flush();
//关闭资源
fileInputStream.close();
fileInputStream.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
先运行服务端,在运行客户端
。测试结果发送成功:
先运行服务端,在运行客户端
package com.lijie;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
//UDP协议Socket:服务端
public class Server1 {
public static void main(String[] args) {
try {
//DatagramSocket代表声明一个UDP协议的Socket
DatagramSocket socket = new DatagramSocket(8888);
//byte数组用于数据存储。
byte[] car = new byte[1024];
//DatagramPacket 类用来表示数据报包DatagramPacket
DatagramPacket packet = new DatagramPacket(car, car.length);
// //创建DatagramPacket的receive()方法来进行数据的接收,等待接收一个socket请求后才执行后续操作;
System.out.println("等待UDP协议传输数据");
socket.receive(packet);
//packet.getLength返回将要发送或者接收的数据的长度。
int length = packet.getLength();
System.out.println("啥东西来了:" + new String(car, 0, length));
socket.close();
System.out.println("UDP协议Socket接受成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
package com.lijie;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
//UDP协议Socket:客户端
public class Client1 {
public static void main(String[] args) {
try {
//DatagramSocket代表声明一个UDP协议的Socket
DatagramSocket socket = new DatagramSocket(2468);
//字符串存储人Byte数组
byte[] car = "UDP协议的Socket请求,有可能失败哟".getBytes();
//InetSocketAddress类主要作用是封装端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
//DatagramPacket 类用来表示数据报包DatagramPacket
DatagramPacket packet = new DatagramPacket(car, car.length, address);
//send() 方法发送数据包。
socket.send(packet);
System.out.println("UDP协议的Socket发送成功");
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
先运行服务端,在运行客户端
。测试结果成功发送成功:
ServerSocket表示为服务端,主要作用就是绑定并监听一个服务器端口,为每个建立连接的客户端“克隆/映射”一个Socket对象,具体数据操作都是通过这个Socket对象完成的,ServerSocket只关注如何和客户端建立连接
构造方法 | 描述 |
---|---|
public ServerSocket(SocketImpl impl) | ServerSocket套接字的实际工作由SocketImpl类的实例执行 |
public ServerSocket(int port) | 指定端口来绑定服务器套接字 |
public ServerSocket(int port, int backlog) | 利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号 |
public ServerSocket(int port, int backlog, InetAddress address) | 使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器 |
public ServerSocket() | 创建非绑定服务器套接字 |
实际方法 | 描述 |
---|---|
public int getLocalPort() | 返回此套接字在其上侦听的端口 |
public Socket accept() | 侦听并接受到此套接字的连接,信息和数据都是通过这个Socket对象获取的 |
public void setSoTimeout(int timeout) | 通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位 |
public void bind(SocketAddress host, int backlog) | 将 ServerSocket 绑定到特定地址(IP 地址和端口号) |
Socket类同时工作于客户端和服务端,所有方法都是通用的,这个类三个主要作用,校验包信息,发起连接(Client),操作流数据(Client/Server)
构造方法 | 描述 |
---|---|
public Socket(String host, int port) | 创建一个流套接字并将其连接到指定主机上的指定端口号 |
public Socket(InetAddress host, int port) | 创建一个流套接字并将其连接到指定 IP 地址的指定端口号 |
public Socket(String host, int port, InetAddress localAddress, int localPort) | 创建一个套接字并将其连接到指定远程主机上的指定远程端口 |
public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) | 创建一个套接字并将其连接到指定远程地址上的指定远程端口 |
public Socket() | 通过系统默认类型的 SocketImpl 创建未连接套接字 |
实际方法 | 描述 |
---|---|
public void connect(SocketAddress host, int timeout) | 将此套接字连接到服务器,并指定一个超时值 |
public InetAddress getInetAddress() | 返回套接字连接的地址 |
public int getPort() | 返回此套接字连接到的远程端口 |
public int getLocalPort() | 返回此套接字绑定到的本地端口 |
public SocketAddress getRemoteSocketAddress() | 返回此套接字连接的端点的地址,如果未连接则返回 null |
public InputStream getInputStream() | 返回此套接字的输入流,常使用DataOutputStream做容器 |
public OutputStream getOutputStream() | 返回此套接字的输出流,常用DataOutputStream |
public void close() | 关闭此套接字,来自Closeable |
DatagramSocket 类用于表示发送和接收数据报包的套接字。
构造方法 | 描述 |
---|---|
DatagramSocket() | 构造数据报包套接字并将其绑定到本地主机上任何可用的端口。 |
DatagramSocket(int port) | 创建数据报包套接字并将其绑定到本地主机上的指定端口。 |
DatagramSocket(int portJnetAddress addr) | 创建数据报包套接字,将其绑定到指定的本地地址。 |
DatagramSocket(SocketAddress bindaddr) | 创建数据报包套接字,将其绑定到指定的本地套接字地址。 |
实际方法 | 描述 |
---|---|
void bind(SocketAddress addr) | 将此 DatagramSocket 绑定到特定的地址和端口。 |
void close() | 关闭此数据报包套接字。 |
void connect(InetAddress address,int port) | 将套接字连接到此套接字的远程地址。 |
void connect(SocketAddress addr) | 将此套接子连接到远程套接子地址(IP地址+端口号)。 |
void disconnect() | 断开套接字的连接。 |
InetAddress getInetAddress() | 返回此套接字连接的地址。 |
InetAddress getLocalAddress() | 获取套接字绑定的本地地址。 |
int getLocalPort() | 返回此套接字绑定的本地主机上的端口号。 |
int getPort() | 返回此套接字的端口。 |
DatagramPacket 类用来表示数据报包,数据报包用来实现无连接包投递服务。
构造方法 | 描述 |
---|---|
DatagramPacket(byte[] buf,int length) | 构造 DatagramPacket,用来接收长度为 length 的数据包。 |
DatagramPacket(byte[] buf,int offset, int length) | 构造 DatagramPacket,用来接收长度为 length 的包,在缓冲区中指定了偏移量。 |
DatagramPacket(byte[] buf,int length,InetAddress address,int port) | 构造 DatagramPacket,用来将长度为 length 的包发送到指定主机上的指定端口。 |
DatagramPacket(byte[] buf,int length,SocketAddress address) | 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口。 |
DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port) | 构造 DatagramPacket,用来将长度为 length 偏移量为 offset的包发送到指定主机上的指定端口。 |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造数据报包,用来将长度为 length、偏移量为 offset 的包发送到指定主机上的指定端口。 |
实际方法 | 描述 |
---|---|
InetAddress getAddress() | 返回某台机器的 IP 地址,此数据报将要发往该机器或者从该机器接收。 |
byte[] getData() | 返回数据缓冲区。 |
int getLength() | 返回将要发送或者接收的数据的长度。 |
int getOffset() | 返回将要发送或者接收的数据的偏移量。 |
int getPort() | 返回某台远程主机的端口号,此数据报将要发往该主机或者从该主机接收。 |
getSocketAddress() | 获取要将此包发送或者发出此数据报的远程主机的SocketAddress(通常为 IP地址+端口号)。 |
void setAddress(InetAddress addr) | 设置要将此数据报发往的目的机器的IP地址。 |
void setData(byte[] buf) | 为此包设置数据缓冲区。 |
void setData(byte[] buf,int offset,int length) | 为此包设置数据缓冲区。 |
void setLength(int length) | 为此包设置长度。 |
void setPort(int port) | 设置要将此数据报发往的远程主机的端口号。 |
void setSocketAddress(SocketAddress address) | 设置要将此数据报发往的远程主机的SocketAddress (通常为 IP地址+端口号)。 |
Java提供了InetAddress类来代表互联网协议(IP)地址,InetAddress类没有提供构造器,而是提供了如下两个静态方法来获取InetAddress实例:
实际方法 | 描述 |
---|---|
getLocalHost() | 获得一个InetAddress对象,该对象含有本地机的域名和IP地址。 |
getByName(String host) | 根据主机名获取对应的InetAddress对象。 |
getByAddress(byte[] addr) | 根据原始IP地址来获取对应的InetAddress对象 |
getAddress() | 获取字节数组形式的IP地址 |
代码示例:
package com.lijie;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
public class Test {
public static void main(String[] args) throws UnknownHostException {
//InetAddress.getLocalHost();该对象含有本地机的域名和IP地址
InetAddress address = InetAddress.getLocalHost();
System.out.println("计算机名" + address.getHostName());
System.out.println("IP地址" + address.getHostAddress());
//根据机器名获取InetAddress实例
InetAddress address2 = InetAddress.getByName("MS-CFPIYPGYRLOM");
System.out.println("IP地址" + address2.getHostAddress());
//根据ip地址获取InetAddress实例
InetAddress address3 = InetAddress.getByName("192.168.157.1");
System.out.println("计算机名" + address3.getHostName());
}
}
InetSocketAddress
InetSocketAddress是SocketAddress的实现子类。此类实现 IP 地址(IP 地址 + 端口号)的创建,不依赖任何协议。
在使用Socket来连接服务器时最简单的方式就是直接使用IP和端口,但Socket类中并未提供这种方式,而是靠SocketAddress的子类InetSocketAddress来实现 IP 地址 + 端口号的创建,不依赖任何协议。
//使用方式
SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
SocketAddress address2 = new InetSocketAddress("120.77.0.58", 0000);