网络编程基础
- 计算机网络: 两台或者更多台计算机组成的网络, 在同一个网络中, 任意两台中计算机可以直接通信, 所有计算机都需要遵守同一种网络协议.
- 计算机接入互联网, 必须使用TCP/IP协议
- TCP/IP协议泛指互联网协议, 最重要的两个协议: TCP协议和IP协议
IP地址
- IP地址用于唯一标识一个网络接口
- IPV4: 采用32位地址; IPV6: 采用128位地址
127.0.0.1
: 指向本机- 公网IP地址和内网IP地址. 公网ip可以直接访问, 内网ip只能内部网络访问
(10.*.*.*)``192.168.*.*
- 如果有一个网卡, 并接入了网络, 就有了一个本机地址, 和ip地址, 可以使用这个ip地址接入网络
- 如果有两个网卡就可以分别接入不同的网络, 例如路由器和交换机
- 如果两台计算机位于同一个网络, 那么他们可以直接通信, 因为ip地址的前段是相同的, 也就是网络号是相同的.
- 网络号: ip地址通过子网掩码过滤后得到的.
IP = 101.202.99.2
Mask = 255.255.255.0
Network = IP & Mask = 101.202.99.0
- 网络号相同, 证明在同一个网路, 可以直接通信
- 网络号不同, 需要路由器/交换机进行通讯. 也就是网关
- 网关的作用是链接多个网络
- 路由: 负责把一个网络的数据包发送到另一个网络
域名
- 域名解析服务器DNS负责把域名翻译成对应的IP, 客户端再根据IP访问
nslookup [url]
nsloopkup www.baidu.com
Server: 192.168.16.1
Address: 192.168.16.1#53
Non-authoritative answer:
www.baidu.com canonical name = www.a.shifen.com.
Name: www.a.shifen.com
Address: 61.135.169.121
Name: www.a.shifen.com
Address: 61.135.169.125
网络模型
- OSI网络模型为了简化网络各层操作, 提供标准接口便于实现和维护
- 模型从上到下:
- 应用层: 提供应用程序之间的通讯
- 表示层: 处理数据格式, 加解密等
- 会话层: 负责建立和维护会话
- 传输层: 提供端到端的可靠传输
- 网络层: 负责根据目标地址选择路由来传输数据
- 链路层和物理层: 负责把数据进行分片并真正通过物理网络传输
- TCP协议:
- 应用层; 传输层; IP层; 网络接口层;
常用协议
-
IP协议: 一个分组协议, 不保证可靠传输
-
TCP协议: 建立在IP协议之上, 传输控制协议, 面向链接的协议, 支持可靠传输和双向通信
-
ip协议只负责发送数据包, 不保证顺序和正确性;
-
tcp协议负责控制数据包传输, 在传输数据之前要先建立连接, 建立链接后才能传输数据, 传输完成后, 断开链接
-
tcp通过接受确认, 超时重传等机制保证数据的可靠传输
-
tcp允许双向通信, 即通信的双方可以同时发送和接受数据
-
tcp协议是应用最广的协议, http和smtp都是建立在tcp协议之上
-
udp协议是一种数据报文协议, 无连接协议, 不保证可靠传输. 在通讯前不需要建立连接, 因此传输效率比tcp高, 并比tcp协议简单; 传输的数据, 需要能够忍受丢失.
TCP编程
- 一个应用程序通过一个Socket建立一个远程连接, Socket内部通过TCP/IP协议把数据传输到网络
- Socket, TCP, 和部分IP的功能由操作系统提供, 不同的编程语言只是提供了对操作系统的简单封装.
- 操作系统抽出Socket接口, 每个应用程序对应不同的Socket, 数据包才能正确地发到对应的应用程序
- socket: ip + 端口号. 小于1024的属于
特权端口
- 使用Socket进行网络编程的本质, 是两个进程之间的网络通信.
- 一个进程充当服务器, 主动监听某个指定的端口
- 一个进程充当客户端, 主动链接服务器的ip地址和指定端口
- 连接成功后, 服务器和客户端成功建立一个TCP连接, 双方后续就可以随时发送和接受数据
- Socket成功建立后:
- 服务端socket是指定的ipd地址和指定的端口号
- 客户端的socket是它所在的计算机的ip地址和一个由操作系统分配的随机端口号.
案例运行顺序
- 启动服务器, 监听端口, 一直查看有没有其他socket访问这个端口的.
- 有, 便开启一个新的线程 | 客户端链接这个socket
- 获取socket的输入和输出 | 客户端获取socket的输入和输出
- 服务器端向socket写入'hello'
- 客户端读取socket中数据
[server]: hello
- 客户端获取屏幕输入的数据, 并写入socket中
- 服务器端接受到socket中数据, 修改后, 再次写入socket中
- 客户端收到socket中数据, 并打印在屏幕上:
ok: ...
UDP编程
- udp编程没有创建连接, 数据包一次收发一个, 没有流的概念
- udp编程使用socket, 指定ip和端口号. 但是和tcp是两套完全独立的端口.
HTTP编程
-
超文本传输协议, 基于HTTP协议之上的一种请求-响应协议.
-
客户端和服务器端首先建立TCP链接, 服务器总是使用
80
和安全的443
端口 -
然后客户端向服务器发送一个HTTP请求, 服务器端接受后, 返回一个HTTP响应, 包含HTML数据
-
客户端解析HTML后展示给用户.
-
HTTP协议是固定的, 由header和body组成
-
Header:
- 第一行总是:
请求方法 路径 HTTP版本号
- 后面总是:
Header: value
格式 - Host: 表示请求的域名, 因为一台服务器上可能含有多个网站, 因此有必要依靠Host进行区分
- User-Agent: 客户端自身标识
- Accept: 表示客户端可以处理的HTTP响应格式
- Accept-Language: 表示客户端接受的语言.
- 第一行总是:
-
Get请求, 只有header没有body.
-
POST请求通常要设置
Content-Type
表示body类型.Content-Length
表示内容长度. -
POST请求中代用body, 用一个空行分割
-
GET请求的参数必须放到url上, 有长度限制
-
响应的第一行总是:
HTTP版本 响应代码 响应说明
- 1**: 提示行响应, eg: 101: 切换协议, 常见WebSocket协议
- 2**: 表示成功: 200表示成功, 206表示发送了部分内容
- 3**: 表示重定向: eg: 301表示永久重定向, 303表示客户端应该按照指定路径重新发送请求
- 4**: 表示因为客户端问题导致的错误
- 5**: 表示服务器端错误
-
请求图片的时候, 图片会把二进制内容发送给客户端
-
服务器总是被动的接受客户端的请求, 并并响应他
-
http1.1允许在一个tcp请求中反复发送-响应
-
http2.0允许同时发送多个请求, 返回的时候, 不一定按照顺序返回
java的Http编程
- 只讨论客户端的http编程
- 早期JDK使用
HttpURLConnection
进行实现
URL url = new URL("http://www.baidu.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.setConnectTimeout(5000);
// 设置http请求头
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)");
// 发送http请求
conn.connect();
// if (conn.getResponseCode() != 200) {
// throw new RuntimeException();
// }
// 获取所有的响应
Map> map = conn.getHeaderFields();
for (String key: map.keySet()) {
System.out.println(key+": "+map.get(key));
}
// 获取响应内容
InputStream input = conn.getInputStream();
public class Client {
static HttpClient httpClient = HttpClient.newBuilder().build(); // 创建全局实例, 内部使用线程池优化多个http连接
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException {
String url = "https://www.sina.com.cn";
HttpRequest request = HttpRequest.newBuilder(new URI(url))
.header("User-Agent", "Java HttpClient")
.header("Accept", "*/*")
.timeout(Duration.ofSeconds(5))
.version(Version.HTTP_2)
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// Http允许重复的header, 因此一个header可对应多个value
Map> headers = response.headers().map();
for(String header: headers.keySet()) {
System.out.println(header + ": " + headers.get(header).get(0));
}
System.out.println(response.body().substring(0, 1024) + "...");
}
}
- 获取图片, 使用
HttpResponse.BodyHandlers.ofByteArray()
进行转换 - 响应内容很大的时候, 可以使用
HttpResponse.BodyHandlers.ofInputStream()
获取一个数据流进行加载 - 使用
POST
请求, 正确设置Content-type
即可
发送邮件
- 各个过程:
- MUA: Mail User Agent
- MTA: Mail Transfer Agent
- MDA: Mail Delivery Agent
- mua给mta发送邮件的协议是
SMTP
协议 simple mail transfer protocal协议, 使用25端口, 或者加密端口465或587 - stmp建议在tcp协议之上
- 一个Multipart可以添加若开个
bodypart
, 第一个是正文, 后面是附件
接受邮件
- 接受邮件使用POP3/IMAP协议
RMI远程调用
- 一个jvm可以通过网络实现远程调用另一个jvm的方法
- remote method invocation
- 实现RMI服务端和客户端共享一个接口
- 此接口必定派生自
java.rmi.Remote
, 并在每个方法中声明RemoteException
public class Server {
public static void main(String[] args) throws RemoteException {
System.out.println("create World clock remote service...");
// 实例化一个WorldClock:
WorldClock worldClock = new WorldClockService();
// 将此服务转换为远程服务接口:
WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock, 0);
// 将RMI端口注册到1099端口
Registry registry = LocateRegistry.createRegistry(1099);
// 注册此服务, 服务为"WorldClock"
registry.rebind("WorldClock", skeleton);
}
}
// 客户端调用方法, 通过这个实现类返回结果
public class WorldClockService implements WorldClock {
@Override
public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException{
return LocalDateTime.now(ZoneId.of(zoneId)).withNano(0);
}
}
public interface WorldClock extends Remote {
LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}
// 客户端只有接口, 并没有实现类, 使用的是服务端实现类获得数据
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 连接到服务器, 端口1099
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找服务, 并进行强制转换
WorldClock worldClock = (WorldClock) registry.lookup("WorldClock");
// 正常调用接口方法
LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai");
System.out.println(now);
}
}
- 客户端对应的
WorldClock
对应了一个实现类, 由Registry
动态生成, 并把方法调用通过网络传递到服务端. - 服务器接受到的网络调用的服务并不是我们自己编写的
WorldClockService
, 而是Registry
自动生成的代码, 我们把客户端的实现类称为stub
, 而服务端的网络服务类称为skeleton
, 会真正调用worldClockService
, 获得结果. - rmi严重依赖序列化和反序列化, 这种情况下会造成漏洞, 因此必须是双方信任的内网机器