网络编程是开发中经常需要用到的一个点,也是面试中必考题,本节对网络编程中常用知识点进行简要概述。
原文链接网络编程基础(上)
网络编程基础之UDP编程请看UDP
以下知识点均来源于廖雪峰官方网站https://www.liaoxuefeng.com/wiki/1252599548343744/1323711850348577
网络模型
计算机网络
计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。
网络模型
由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI(Open System Interconnect) 网络模型是ISO组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:
- 应用层,提供应用程序之间的通信;
- 表示层:处理数据格式,加解密等等;
- 会话层:负责建立和维护会话;
- 传输层:负责提供端到端的可靠传输;
- 网络层:负责根据目标地址选择路由来传输数据;
- 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。
TCP/IP 模型
互联网实际使用的TCP/IP模型并不是对应到OSI的7层模型,而是大致对应OSI的5层模型:
OSI | TCP/IP |
---|---|
应用层 | 应用层 |
表示层 | |
会话层 | |
传输层 | 传输层 |
网络层 | IP层 |
链路层 | 网络接口 |
物理层 | 物理 |
小结
- 计算机网络:由两台或更多计算机组成的网络;
- 互联网:连接网络的网络;
- IP地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
- 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
- 网络协议:互联网使用TCP/IP协议,它泛指互联网协议簇;
- IP协议:一种分组交换传输协议;
- TCP协议:一种面向连接,可靠传输的协议;
- UDP协议:一种无连接,不可靠传输的协议。
TCP 编程
Socket
在开发网络应用程序的时候,我们又会遇到Socket这个概念。Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络:
Socket、TCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如,Java提供的几个Socket相关的类就封装了操作系统提供的接口。
为什么需要Socket进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。
一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配,它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打开。
使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。
因此,当Socket连接成功地在服务器端和客户端之间建立后:
- 对服务器端来说,它的Socket是指定的IP地址和指定的端口号;
- 对客户端来说,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。
服务端实现
要使用Socket编程,我们首先要编写服务器端程序。Java标准库提供了ServerSocket来实现对指定IP和指定端口的监听。ServerSocket的典型实现代码如下:
/**
* @Auther Mario
* @Date 2020-12-30 12:38
* @Version 1.0
* Socket 通信编程之TCP
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666);
System.out.println("服务端口正在监听");
for(;;){
//处理每个新的请求
Socket sock = ss.accept();
System.out.println("已经接受客服端请求");
//每个请求分配一个线程处理
Thread t = new Handle(sock);
t.start();
}
}
}
class Handle extends Thread{
private Socket socket;
public Handle(Socket socket) {
this.socket = socket;
}
public void run(){
try(InputStream inputStream = this.socket.getInputStream()){
try(OutputStream outputStream = this.socket.getOutputStream()){
handle(inputStream,outputStream);
}
}catch (Exception e){
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
System.out.println("client disconnected");
}
}
private void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
bufferedWriter.write("hello\n");
bufferedWriter.flush();
for(;;){
//读取请求的每一行数据,直到关键字'bye',发送数据
String read = bufferedReader.readLine();
if(read.equals("bye\n")){
bufferedWriter.write("bye\n");
bufferedWriter.flush();
break;
}
bufferedWriter.write("ok: " + read + "bye\n");
bufferedWriter.flush();
}
}
}
服务器端通过代码:
ServerSocket ss = new ServerSocket(6666);
在指定端口「6666」监听。这里我们没有指定IP地址,表示在计算机的所有网络接口上进行监听。
如果ServerSocket监听成功,我们就使用一个无限循环来处理客户端的连接:
for (;;) {
Socket sock = ss.accept();
Thread t = new Handler(sock);
t.start();
}
注意到代码ss.accept()表示每当有新的客户端连接进来后,就返回一个Socket实例,这个Socket实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。
我们在多线程编程的章节中介绍过线程池,这里也完全可以利用线程池来处理客户端连接,能大大提高运行效率。
如果没有客户端连接进来,accept()方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket会把连接扔到队列里,然后一个一个处理。对于Java程序而言,只需要通过循环不断调用accept()就可以获取新的连接。
客服端实现
/**
* @Auther mashang
* @Date 2020-12-30 14:04
* @Version 1.0
* Socket 通信编程 TCP
*/
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",6666);
// 用于读取网络数据:
try(InputStream inputStream = socket.getInputStream()){
// 用于写入网络数据:
try(OutputStream outputStream = socket.getOutputStream()){
handle(inputStream,outputStream);
}
}
socket.close();
System.out.println("disconnected");
}
private static void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server]: " + bufferedReader.readLine());
for(;;){
System.out.println(">>>");
String s = scanner.nextLine();
bufferedWriter.write(s);
bufferedWriter.newLine();
bufferedWriter.flush();
String resp = bufferedReader.readLine();
System.out.println("<<<" + resp);
if(resp.equals("bye")){
break;
}
}
}
}
客户端程序通过:
Socket sock = new Socket("localhost", 6666);
连接到服务器端,注意上述代码的服务器地址是**“localhost”** ,表示本机地址,端口号是「6666」。如果连接成功,将返回一个Socket实例,用于后续通信。
自我总结:通过以上对服务端和客服端简要实现得知,服务端在6666端口一直处于监听状态,等待客服端连接,收到客户端请求后新建一个Handle线程处理请求,该线程主要处理服务端通过socket流进行网络通信,inputStream从客户端接收的输入流,outputStream发给客户端的输出流,handle()方法处理输入输出流,在通信结束后关闭socket连接。handle()把inputStream和outputStream分别包装为bufferedReader和bufferedWriter,建立连接后服务端首先发送"hello"给客户端bufferedWriter.write(“hello\n”),flush()用于强制把缓冲区数据发送出去,然后一直等待客户端发送数据,进而读取数据,回复信息。客户端则连接服务端端口建立通信,通过socket传递数据,从输入台输入数据,flush()发送数据,通过bufferedReader.readLine()读取数据,直到“bye”完成通信,断开连接。
Socket流
当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket实例进行网络通信。因为TCP是一种基于流的协议,因此,Java标准库使用InputStream和OutputStream来封装Socket的数据流,这样我们使用Socket的流,和普通IO流类似:
// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();
最后我们重点来看看,为什么写入网络数据时,要调用flush()方法。
如果不调用flush(),我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()强制把缓冲区数据发送出去。
小结
使用Java进行TCP编程时,需要使用Socket模型:
- 服务器端用ServerSocket监听指定端口;
- 客户端使用Socket(InetAddress, port)连接服务器;
- 服务器端用accept()接收连接并返回Socket;
- 双方通过Socket打开InputStream/OutputStream读写数据;
- 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
- flush()用于强制输出缓冲区到网络。