入门知识
软件结构
- C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
- B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。
网络编程:就是在一定的协议下,实现两台计算机的通信的程序。
网络通信的大致流程为:一个数据包经由应用程序产生,进入到协议栈中进行各种报文头的包装,然后操作系统调用网卡驱动程序指挥硬件,把数据发送到对端主机。
整个过程的大体的图示如下:
协议栈其实是位于操作系统中的一些协议的堆叠,这些协议包括 TCP、UDP、ARP、ICMP、IP等。
通常某个协议的设计都是为了解决特定问题的,比如:
- TCP 的设计就负责安全可靠的传输数据
- UDP 设计就是报文小,传输效率高
- ARP 的设计是能够通过 IP 地址查询物理(Mac)地址
- ICMP 的设计目的是返回错误报文给主机
- IP 设计的目的是为了实现大规模主机的互联互通
网络通信常见协议:UDP|TCP
UDP:面向无连接的协议,通信的双方不用建立连接,可以直接发送数据
- 好处:效率高,耗资小
- 弊端:不安全,容易丢失数据
TCP:面向连接协议,客户端和服务器端必须经过3次握手建立逻辑连接,才能通信
- 好处:安全
- 弊端:效率低
三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。// 服务器你死了吗?
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。// 我活着 啊!!
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。// 我知道了!!
TCP/IP 协议
TCP/IP协议参考模型
TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中
- 应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
- 传输层:TCP,UDP
- 网络层:IP,ICMP,OSPF,EIGRP,IGMP
- 数据链路层:SLIP,CSLIP,PPP,MTU
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务。
IP地址
IP地址:就相当于计算机的身份号(唯一)
ip地址的作用:具有唯一性,在网络中可以通过ip地址找到另外一台计算机
ip地址分类:
- ipv4:ip地址由4个字节组成,一个字节8位(比特位 1,0)
二进制:11001101.11001100.11000001.11001111
十进制:192.168.0.106
每个字节的范围:0-255(2^8),ip地址第一位不能为0
ip地址的数量:42亿(2^32=4294967296个)
问题:随着计算机的增多,ip地址面临枯竭(全球IPv4地址在2011年2月分配完毕)不够用,就出了ipv6地址
- ipv6:ip地址由16个字节组成,一个字节8位(比特位 1,0)
ip地址的数量:2^128=3.4028236692093846346337460743177e+38
号称可以为地球上每一粒沙子编写一个ip地址
为了表示方便使用十六进制:fe80::a8a6:b83c:8b8b:2685%17
一些常用dos命令:dos窗口 win+r ==> cmd ==> dos窗口
1.查看电脑的IP信息
命令:ipconfig
--------------------------------------------------
Windows IP 配置
连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::a8a6:b83c:8b8b:2685%17
IPv4 地址 . . . . . . . . . . . . : 192.168.0.106
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.0.1
--------------------------------------------------
2.测试你的电脑和指定ip地址的电脑是否可以连通
命令:ping ip地址
--------------------------------------------------
C:\Users\Administrator>ping 192.168.0.222 没有ping通
正在 Ping 192.168.0.222 具有 32 字节的数据:
来自 192.168.0.106 的回复: 无法访问目标主机。
来自 192.168.0.106 的回复: 无法访问目标主机。
来自 192.168.0.106 的回复: 无法访问目标主机。
来自 192.168.0.106 的回复: 无法访问目标主机。
--------------------------------------------------
C:\Users\Administrator>ping www.baidu.com
正在 Ping www.a.shifen.com [61.135.169.121] 具有 32 字节的数据:
来自 61.135.169.121 的回复: 字节=32 时间=6ms TTL=56
来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56
来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56
来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56
--------------------------------------------------
3.ping本机的ip地址(你自己电脑的ip地址)
命令:ping 127.0.0.1 或 ping localhost
端口号
端口号是一个逻辑端口,无法直接看到,使用一些软件可以看到(电脑管家,360.…)
当打开网络软件(联网使用)时,操作系统就会为这个网络软件分配一个随机的端口号或者网络软件在打开的时候和操作系统要指定的端口号
端口号是由两个字节组成,表示的范围:2^16=0-65535 之间
1024之前的端口号,不能使用,已经被操作系统分配给一些已知的网络软件。
注意:各个网络软件的端口号是不能重复
常用的端口号:
- 80端口:网络端口
- 数据库:mysql:3306, oracle:1521
- Tomcat服务:8080
保证数据能准确无误发送到对方计算机的某一个软件上,使用 ip地址:端口号
测试端口号是否连通:telnet ip地址:端口号
InetAddress类:获取IP地址
java.net.InetAddress:描述计算机的ip地址
此类表示互联网协议 (IP) 地址。
可以使用InetAddress类中的方法获取到计算机的ip地址
创建对象的方式:静态方法
static InetAddress getLocalHost() // 返回本地主机(你自己电脑的ip地址对象)。
static InetAddress getByName(String host) // 在给定主机名的情况下确定主机的 IP 地址。
/* 参数:
String host:可以传递主机名称、ip地址、域名
*/
非静态的方法:
String getHostAddress() // 返回 IP 地址字符串(以文本表现形式)。
String getHostName() // 获取此 IP 地址的主机名。
Socket:套接字
应用程序比如浏览器、电子邮件、文件传输服务器等产生的数据,会通过传输层协议进行传输。而应用程序是不会和传输层直接建立联系的,而是有一个能够连接应用层和传输层之间的套件,这个套件就是 Socket。
阻塞/非阻塞、同步/异步
- 阻塞:等待结果,什么事都不能做
- 非阻塞:可以做别的事情
- 同步:主动获取结果
- 异步:等待通知结果
- BIO:Block(阻塞的) IO 【同步、阻塞】
- NIO:Non-Block(非阻塞的)(同步)IO 【同步、非阻塞】——JDK1.4开始
- AIO:Asynchronous(异步-非阻塞)IO 【异步、非阻塞】 ——JDK1.7开始
TCP/IP 通信
TCP 通信的客户端:Socket
作用:主动和服务器经过3次握手建立连接通路,给服务器发送数据,读取服务器回写的数据
表示客户端的类:java.net.Socket:此类实现客户端套接字(也可以就叫“套接字”)。
套接字:封装了IP地址和端口号的网络单位
构造方法:
public Socket(InetAddress address, int port) // 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
public Socket(String host, int port) // 创建一个流套接字并将其连接到指定主机上的指定端口号。
/* 参数:
InetAddress address | String host:传递服务器的ip地址
int port:服务器的端口号
*/
成员方法:
OutputStream getOutputStream() // 返回此套接字的输出流。
InputStream getInputStream() // 返回此套接字的输入流。
void shutdownOutput() // 禁用此套接字的输出流。
// 对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
注意:
创建客户端Socket对象的时候,客户端会根据服务器的ip地址和端口号和服务器经过三次握手连接连接通路
- 服务器已经启动了,服务器的ip地址和端口号填写正确:握手成功,创建好Socket对象
- 服务器没有启动,服务器的ip地址和端口号填写错误:握手失败,会抛出连接异常
ConnectException: Connection refused: connect
- 客户端和服务器之间进行数据传输,不能使用自己创建的流对象(只能和本地硬盘之间进行读写)。
使用Socket对象中提供的网络流对象
TCP 通信的服务端:ServerSocket
作用:接收客户端的请求和客户端经过3次握手建立连接通路;读取客户端发送的数据,给客户端回写(发送)数据
表示服务器的类:java.net.ServerSocket;此类实现服务器套接字。
构造方法:
public ServerSocket(int port) // 创建绑定到特定端口的服务器套接字。
成员方法:
Socket accept() // 侦听并接受到此套接字的连接。
/* 使用accpet方法,会一直监听客户端的请求
有客户端请求服务器,accept方法就会获取到请求的客户端Socket对象
没有客户端请求服务器,accept方法会进入到阻塞状态,一直等待
*/
注意:
服务器启动的时候,抛出了以下的异常:说明服务器使用的端口号已经被占用了,需要更换端口号
java.net.BindException: Address already in use: JVM_Bind
文件上传案例
文件上传的客户端
读取本地文件,上传到服务器中,读取服务器回写的"上传成功!"
文件上传就是文件的复制:
数据源:c:\1.jpg
目的地:服务器中
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Demo01TCPClient {
public static void main(String[] args) throws IOException {
//1.创建本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("c:\\1.jpg");
//2.创建客户端Socket对象,构造方法绑定服务器的ip地址和端口号
Socket socket = new Socket("127.0.0.1", 9999);
//3.使用客户端Socket对象中的方法getOutputStream,获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//4.使用本地字节输入流FileInputStream对象中的方法read,读取要上传的而文件
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1){
//5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器中
os.write(bytes, 0, len);
}
// 上传结束
socket.shutdownOutput();
//6.使用客户端Socket对象中的方法getInputStream,获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//7.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的"上传成功!"
while ((len = is.read(bytes)) != -1){
System.out.println(new String(bytes, 0, len));
}
//8.释放资源(FileInputStream对象, Socket)
fis.close();
socket.close();
}
}
文件上传的服务器端(多线程)
读取客户端上传的文件,把文件保存到服务器的硬盘上,给客户端回写"上传成功!"
文件上传就是文件的复制:
数据源: 客户端上传的文件 1.jpg
目的地: 服务器的硬盘中 d:\upload\1.jpg
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Demo02TCPServer {
public static void main(String[] args) throws IOException {
//1.判断d盘有没有upload文件夹,没有则创建
File file = new File("d:\\upload");
if(!file.exists()){
file.mkdir();
}
//2.创建服务器ServerSocket对象,和系统要指定的端口号9999
ServerSocket server = new ServerSocket(9999);
// 一直循环监听客户端的请求(轮询)
while(true){
//3.使用服务器ServerSocket对象中的方法accpet,监听并获取请求的客户端Socket对象
Socket socket = server.accept();
// 开启一个新的线程完成这个客户端的文件上传
new Thread(()->{
try {
//4.使用客户端Socket对象中的方法getInputStream,获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
/*
自定义一个文件的名称,防止名称的重复,覆盖之前的文件
规则:不重复 ==> 自己写 ==> 域名 + 毫秒值 + 随机数
*/
String fileName = "cormorant" + System.currentTimeMillis()
+ new Random().nextInt(9999999) + ".jpg";
//5.创建本地字节输出流FileOutputStream对象,绑定要输出的目的地
//FileOutputStream fos = new FileOutputStream(file + "\\1.jpg"); //d:\\upload\\1.jpg
FileOutputStream fos = new FileOutputStream(file + File.separator + fileName);
//6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes)) != -1){
//7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件,写到服务器的硬盘中保存
fos.write(bytes, 0, len);
}
//8.使用客户端Socket对象中的方法getOutputStream,获取网络字节输出流OutputStream对象
//9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功!"
socket.getOutputStream().write("上传成功".getBytes());
//10.释放资源(fos, Socket, ServerScoket)
fos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
//让服务器一直启动,不在关闭了
//server.close();
}
}
文件上传的阻塞问题
/*
解决:上传完图片之后,给服务器写一个结束标记,告之服务器文件已经上传完毕,无需在等待了
Socket对象中的方法
void shutdownOutput() 禁用此套接字的输出流。
对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
*/
socket.shutdownOutput();
Socket 通信
概述
Linux 内核协议簇中有几十种通讯协议,AF-INET就是常见 TCP/IP 的通讯方式,AF-UNIX 是用于本机线程间通讯一种IPC机制,从用户角度看,所采用通讯模式相差不大,但就原理上看,相差较大。
AF_INET 域 socket 通信过程
典型的TCP/IP四层模型的通信过程:
发送方、接收方依赖 IP:Port 来标识,即将本地的 socket 绑定到对应的 IP 端口上,发送数据时,指定对方的 IP 端口,经过Internet,可以根据此 IP 端口最终找到接收方;接收数据时,可以从数据包中获取到发送方的IP端口。
发送方通过系统调用 send() 将原始数据发送到操作系统内核缓冲区中。内核缓冲区从上到下依次经过TCP层、IP层、链路层的编码,分别添加对应的头部信息,经过网卡将一个数据包发送到网络中。经过网络路由到接收方的网卡。网卡通过系统中断将数据包通知到接收方的操作系统,再沿着发送方编码的反方向进行解码,即依次经过链路层、IP层、TCP层去除头部、检查校验等,最终将原始数据上报到接收方进程。
AF_UNIX 域 socket 通信过程
典型的本地 IPC,类似于管道,依赖路径名标识发送方和接收方。即发送数据时,指定接收方绑定的路径名,操作系统根据该路径名可以直接找到对应的接收方,并将原始数据直接拷贝到接收方的内核缓冲区中,并上报给接收方进程进行处理。同样的接收方可以从收到的数据包中获取到发送方的路径名,并通过此路径名向其发送数据。
异同及应用场景
相同点
- 操作系统提供的接口 socket(),bind(),connect(),accept(),send(),recv(),以及用来对其进行多路复用事件检测的 select(),poll(),epoll() 都是完全相同的。收发数据的过程中,上层应用感知不到底层的差别。
不同点
建立 socket 传递的地址域,及bind()的地址结构稍有区别:
- socket() 分别传递不同的域 AF_INET 和 AF_UNIX
- bind() 的地址结构分别为sockaddr_in(制定IP端口)和 sockaddr_un(指定路径名)
- AF_INET 需经过多个协议层的编解码,消耗系统cpu,并且数据传输需要经过网卡,受到网卡带宽的限制。AF_UNIX 数据到达内核缓冲区后,由内核根据指定路径名找到接收方socket对应的内核缓冲区,直接将数据拷贝过去,不经过协议层编解码,节省系统 cpu,并且不经过网卡,因此不受网卡带宽的限制。
- AF_UNIX 的传输速率远远大于 AF_INET
- AF_INET 不仅可以用作本机的跨进程通信,同样的可以用于不同机器之间的通信,其就是为了在不同机器之间进行网络互联传递数据而生。而 AF_UNIX 则只能用于本机内进程之间的通信。
应用场景
- AF_UNIX 由于其对系统 cpu 的较少消耗,不受限于网卡带宽,及高效的传递速率,本机通信则首选 AF_UNIX 域
- AF_INET 多用于跨机器之间的通信
AFUNIX Server Socket 通信
Java AFUNIXServerSocket类
参考:https://vimsky.com/examples/detail/java-class-org.newsclub.net.unix.AFUNIXServerSocket.html
依赖:
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-core</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-common</artifactId>
<version>2.3.2</version>
</dependency>
使用示例1:run
import org.newsclub.net.unix.AFUNIXServerSocket;
@AllArgsConstructor
public class SocketJob implements Runnable{
private String path;
public void run() throws IOException {
File socketFile = new File(path);
socketFile.deleteOnExit();
final ExecutorService executorService = Executors.newCachedThreadPool();
try (AFUNIXServerSocket server = AFUNIXServerSocket.newInstance()) {
// 绑定路径
server.bind(new AFUNIXSocketAddress(socketFile));
System.out.println("server: " + server);
while (!Thread.interrupted()) {
System.out.println("Waiting for connection...");
executorService.execute(new ClientConnection(this, server.accept()));
}
} finally {
executorService.shutdown();
}
}
}
使用示例2:main
import org.newsclub.net.unix.AFUNIXServerSocket;
public static void main(String[] args) throws IOException {
final File socketFile =
new File(new File(System.getProperty("java.io.tmpdir")), "junixsocket-test.sock");
try (AFUNIXServerSocket server = AFUNIXServerSocket.newInstance()) {
server.bind(new AFUNIXSocketAddress(socketFile));
System.out.println("server: " + server);
while (!Thread.interrupted()) {
System.out.println("Waiting for connection...");
try (Socket sock = server.accept()) {
System.out.println("Connected: " + sock);
try (InputStream is = sock.getInputStream();
OutputStream os = sock.getOutputStream()) {
byte[] buf = new byte[128];
int read = is.read(buf);
System.out.println("Client's response: " + new String(buf, 0, read));
System.out.println("Saying hello to client " + os);
os.write("Hello, dear Client".getBytes());
os.flush();
}
}
}
}
}
socket 通讯消息接收
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* AF_UNIX 域 socket 通讯消息接收
*/
@AllArgsConstructor
@Slf4j
public class SocketMsgReceiveBO implements Runnable {
/**
* AF_UNIX 域 docker socket 客户端
*/
private Socket socketClient;
@Override
public void run() {
log.info("start socket client receive msg");
if (socketClient == null || socketClient.isClosed()){
log.error("socket client is unavailable");
return;
}
StringBuffer acceptMsgBuffer = new StringBuffer();
try (InputStream is = socketClient.getInputStream()) {
byte[] buf = new byte[2048];
int readLenth;
// 退出当前循环依赖于socketClient端主动关闭链接,否则当前线程一致等待通讯
while ((readLenth = is.read(buf)) != -1){
acceptMsgBuffer.append(new String(buf, 0, readLenth, StandardCharsets.UTF_8));
log.info("server accept msg:{}", acceptMsgBuffer);
ArrayList<String> validJsonMsgList = new ArrayList<>();
// 获取socket回调消息中的符合json格式的字符串
String validJsonMsg = getValidJsonStrFromMsg(acceptMsgBuffer.toString());
while (StringUtils.isNotBlank(validJsonMsg)){
// 添加到待解析消息集合中
validJsonMsgList.add(validJsonMsg);
/**
* 比较当前合法json格式的消息和原始消息的长度
* 若一致,则原始消息即为一个完整的符合json格式的消息,清空消息缓存Buffer
* 若不一致,则从原始消息的符合json格式片段的最后一个字符之后开始截取,等待拼接到下一次的消息
*/
String originMsg = acceptMsgBuffer.toString();
if (validJsonMsg.length() == originMsg.length()){
acceptMsgBuffer = new StringBuffer();
break;
} else {
acceptMsgBuffer = new StringBuffer(originMsg.substring(validJsonMsg.length()));
}
log.info("all wait parse valid json msgs:{} and nextBuffer:{}", JSON.toJSONString(validJsonMsgList), acceptMsgBuffer);
// 解析合法的消息
validJsonMsgList.forEach(msg -> parseSocketMsg(msg));
}
}
} catch (Exception e){
log.error("failed to receive socket client msg", e);
} finally {
String msg = acceptMsgBuffer.toString();
if (StringUtils.isNotBlank(msg) && isValidSocketMsg(msg)){
log.info("finally parse last msg:{}", msg);
parseSocketMsg(msg);
}
if (!socketClient.isClosed()){
// 若socket客户端未自动关闭,则主动关闭
try {
socketClient.close();
log.info("success to close socket client");
} catch (IOException e){
log.error("failed to close socket client");
}
}
}
}
/**
* 获取socket回调消息中的合法json格式的字符串
*/
private String getValidJsonStrFromMsg(String msg) {
if (StringUtils.isBlank(msg)){
return "";
}
/**
* 为防止出现粘包现象,对该消息进行循环判断,一旦发现内部存在合法的json格式,即任务是有效的消息
* 依赖每次对消息进行截取长度少1的方式,以保证当不能找到匹配的JSON也能退出循环
*/
while (!isValidSocketMsg(msg) && StringUtils.isNoneBlank(msg)){
// 从0开始截取到当前字符串的最后一个字符。substring(startIndex, endIndex)方法入参前闭后开区间
msg = msg.substring(0, msg.length()-1);
int lastLeftBraceIndex = msg.lastIndexOf("}");
if (lastLeftBraceIndex < 0){
// 当字符串中没有"}"字符时,退出循环
break;
}
// 从0截取到字符串的最后一个"}"字符
msg = msg.substring(0, lastLeftBraceIndex + 1);
}
if (isValidSocketMsg(msg)){
return msg;
}
return "";
}
/**
* 如果接收到的消息非空,且符合json格式,则认为是合法的消息,可以进行解析
*/
private boolean isValidSocketMsg(String msg) {
if(StringUtils.isBlank(msg)) return false;
try {
JSON.parse(msg);
return true;
} catch (Exception e){
return false;
}
}
/**
* 解析符合json格式的消息
*/
private void parseSocketMsg(String msgStr) {
JSONObject msgJsonObj = JSON.parseObject(msgStr);
// TODO 根据约定的通讯字段进行相关业务逻辑
}
}