网络编程入门
什么是⽹络编程
⽹络编程,指⽹络上的主机,通过不同的进程,以编程的⽅式实现⽹络通信(或称为⽹络数据传 
输)。
当然,我们只要满⾜进程不同就⾏;所以即便是同⼀个主机,只要是不同进程,基于⽹络来传输数 
据,也属于⽹络编程。
但是,我们⼀定要明确,我们的⽬的是提供⽹络上不同主机,基于⽹络来传输数据资源: 
• 进程A:编程来获取⽹络资源 
• 进程B:编程来提供⽹络资源
⽹络编程中的基本概念
发送端和接收端 
在⼀次⽹络数据传输时: 
发送端:数据的发送⽅进程,称为发送端。发送端主机即⽹络通信中的源主机。 
接收端:数据的接收⽅进程,称为接收端。接收端主机即⽹络通信中的⽬的主机。 
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是⼀次⽹络数据传输产⽣数据流向后的概念。
请求和响应
⼀般来说,获取⼀个⽹络资源,涉及到两次⽹络数据传输: 
• 第⼀次:请求数据的发送 
• 第⼆次:响应数据的发送。
客⼾端和服务端
服务端:在常⻅的⽹络数据传输场景下,把提供服务的⼀⽅进程,称为服务端,可以提供对外服务。 
客⼾端:获取服务的⼀⽅进程,称为客⼾端。
常⻅的客⼾端服务端模型
最常⻅的场景,客⼾端是指给⽤⼾使⽤的程序,服务端是提供⽤⼾服务的程序: 
1. 客⼾端先发送请求到服务端 
2. 服务端根据请求数据,执⾏相应的业务处理 
3. 服务端返回响应:发送业务处理结果 
4. 客⼾端根据响应数据,展⽰处理结果(展⽰获取的资源,或提⽰保存资源的处理结果
Socket套接字
概念 
Socket套接字,是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。基于Socket套接字的⽹络程序开发就是⽹络编程。
分类 
Socket套接字主要针对传输层协议划分为如下三类:
流套接字:使⽤传输层TCP协议 
TCP,即Transmission Control Protocol(传输控制协议),传输层协议。 
以下为TCP的特点(细节后续再学习):
• 有连接
• 可靠传输 
• ⾯向字节流 
• 有接收缓冲区,也有发送缓冲区 
• ⼤⼩不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是⽆边界的数据,可以多次发送,也可以分开多次接收。
数据报套接字:使⽤传输层UDP协议 
UDP,即User Datagram Protocol(⽤⼾数据报协议),传输层协议。 
以下为UDP的特点(细节后续再学习):
• ⽆连接 
• 不可靠传输 
• ⾯向数据报 
• 有接收缓冲区,⽆发送缓冲区 
• ⼤⼩受限:⼀次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是⼀块⼀块的,发送⼀块数据假如100个字节,必须⼀次发送,接收也必须⼀次接收100个字节,⽽不能分100次,每次接收1个字节。
原始套接字 
原始套接字⽤于⾃定义传输层协议,⽤于读写内核没有处理的IP协议数据。
Java数据报套接字通信模型
对于UDP协议来说,具有⽆连接,⾯向数据报的特征,即每次都是没有建⽴连接,并且⼀次发送全部数据报,⼀次接收全部的数据报。 
java中使⽤UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使⽤ 
DatagramPacket 作为发送或接收的UDP数据报。
Java流套接字通信模型

Socket编程注意事项

1. 客⼾端和服务端:开发时,经常是基于⼀个主机开启两个进程作为客⼾端和服务端,但真实的场 
景,⼀般都是不同主机。 
2. 注意⽬的IP和⽬的端⼝号,标识了⼀次数据传输时要发送数据的终点主机和进程 
3. Socket编程我们是使⽤流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应⽤层协议,也需要考虑,这块我们在后续来说明如何设计应⽤层协议。 
4. 关于端⼝被占⽤的问题 
5. 如果⼀个进程A已经绑定了⼀个端⼝,再启动⼀个进程B绑定该端⼝,就会报错,这种情况也叫端⼝被占⽤。
在cmd输⼊ netstat -ano | findstr 端⼝号 ,则可以显⽰对应进程的pid。
在任务管理器中,通过pid查找进程
解决端⼝被占⽤的问题: 
◦ 如果占⽤端⼝的进程A不需要运⾏,就可以关闭A后,再启动需要绑定该端⼝的进程B 
◦ 如果需要运⾏A进程,则可以修改进程B的绑定端⼝,换为其他没有使⽤的端⼝。
UDP数据报套接字编程
API 介绍
DatagramSocket 
DatagramSocket 是UDP Socket,⽤于发送和接收UDP数据报。 
DatagramSocket 构造⽅法:

DatagramSocket ⽅法:

DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报。 
DatagramPacket 构造⽅法:

DatagramPacket ⽅法:

构造UDP发送的数据报时,需要传⼊ SocketAddress ,该对象可以使⽤ InetSocketAddress 
来创建。
InetSocketAddress
InetSocketAddress ( SocketAddress 的⼦类 )构造⽅法:

UDP Echo Server
public class UdpEchoServer {private DatagramSocket socket = null;// 参数是服务器要绑定的端⼝public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}// 使⽤这个⽅法启动服务器public void start() throws IOException {System.out.println("服务器启动!");while (true) {// 反复的, ⻓期的执⾏针对客⼾端请求处理的逻辑.// ⼀个服务器, 运⾏过程中, 要做的事情, 主要是三个核⼼环节.// 1. 读取请求, 并解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 40socket.receive(requestPacket);// 这样的转字符串的前提是, 后续客⼾端发的数据就是⼀个⽂本的字符串.String request = new String(requestPacket.getData(), 0, requestPacke// 2. 根据请求, 计算出响应String response = process(request);// 3. 把响应写回给客⼾端// 此时需要告知⽹卡, 要发的内容是啥, 要发给谁.DatagramPacket responsePacket = new DatagramPacket(response.getBytesrequestPacket.getSocketAddress());socket.send(responsePacket);// 记录⽇志, ⽅便观察程序执⾏效果.System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAdrequest, response);}}// 根据请求计算响应. 由于是回显程序, 响应内容和请求完全⼀样.public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}UDP Echo Client
public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;// 服务器的 ip 和 服务器的端⼝.public UdpEchoClient(String ip, int port) throws SocketException {serverIp = ip;serverPort = port;// 这个 new 操作, 就不再指定端⼝了. 让系统⾃动分配⼀个空闲端⼝.socket = new DatagramSocket();}// 让这个客⼾端反复的从控制台读取⽤⼾输⼊的内容. 把这个内容构造成 UDP 请求, 发给服务器// 最终再显⽰在客⼾端的屏幕上.public void start() throws IOException {Scanner scanner = new Scanner(System.in);System.out.println("客⼾端启动!");while (true) {// 1. 从控制台读取⽤⼾输⼊的内容System.out.print("-> "); // 命令提⽰符, 提⽰⽤⼾要输⼊字符串.String request = scanner.next();// 2. 构造请求对象, 并发给服务器.DatagramPacket requestPacket = new DatagramPacket(request.getBytes()InetAddress.getByName(serverIp), serverPort);socket.send(requestPacket);// 3. 读取服务器的响应, 并解析出响应内容.DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4socket.receive(responsePacket);String response = new String(responsePacket.getData(), 0, responsePa// 4. 显⽰到屏幕上.System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);// UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090);client.start();}
}UDP Dict Server
编写⼀个英译汉的服务器. 只需要重写process
public class UdpDictServer extends UdpEchoServer {private Map<String, String> dict = new HashMap<>();public UdpDictServer(int port) throws SocketException {super(port);dict.put("cat", "⼩猫");dict.put("dog", "⼩狗");dict.put("fuck", "卧槽");// 可以在这⾥继续添加千千万万个单词. 使每个单词都有⼀个对应的翻译.}// 是要复⽤之前的代码, 但是⼜要做出调整.@Overridepublic String process(String request) {// 把请求对应单词的翻译, 给返回回去.return dict.getOrDefault(request, "该词没有查询到!");}public static void main(String[] args) throws IOException {UdpDictServer server = new UdpDictServer(9090);// start 不需要重新再写⼀遍了. 直接就复⽤了之前的 start !server.start();}
}TCP流套接字编程
和刚才UDP类似. 实现⼀个简单的英译汉的功能
API 介绍
ServerSocket 
ServerSocket 是创建TCP服务端Socket的API。 
ServerSocket 构造⽅法:

ServerSocket ⽅法:

Socket 
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服 
务端Socket。 
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据 的。 
Socket 构造⽅法:

Socket ⽅法:

TCP Echo Server
public class TcpEchoServer {private ServerSocket serverSocket = null;// 这个操作就会绑定端⼝号public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}// 启动服务器public void start() throws IOException System.out.println("服务器启动!");while (true) {Socket clientSocket = serverSocket.accept();processConnection(clientSocket);}}// 通过这个⽅法来处理⼀个连接的逻辑.private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客⼾端上线!\n", clientSocket.getInetAddress().t// 接下来就可以读取请求, 根据请求计算响应, 返回响应三步⾛了.// Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {// ⼀次连接中, 可能会涉及到多次请求/响应while (true) {// 1. 读取请求并解析. 为了读取⽅便, 直接使⽤ Scanner.Scanner scanner = new Scanner(inputStream);if (!scanner.hasNext()) {// 读取完毕, 客⼾端下线.System.out.printf("[%s:%d] 客⼾端下线!\n", clientSocket.getInebreak;}// 这个代码暗含⼀个约定, 客⼾端发过来的请求, 得是⽂本数据, 同时, 还得带有String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回给客⼾端. 把 OutputStream 使⽤ PrinterWriter 包裹⼀下PrintWriter writer = new PrintWriter(outputStream);// 使⽤ PrintWriter 的 println ⽅法, 把响应返回给客⼾端.// 此处⽤ println, ⽽不是 print 就是为了在结尾加上 \n . ⽅便客⼾端writer.println(response);// 这⾥还需要加⼀个 "刷新缓冲区" 操作.writer.flush();// ⽇志, 打印当前的请求详情.System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.gerequest, response);}} catch (IOException e) {e.printStackTrace();} finally {// 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!try {clientSocket.close();} catch (IOException e) {e.printStackTrace();}}}public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9090);server.start();}
}TCP Echo Client
public class TcpEchoClient {private Socket socket = null;// 要和服务器通信, 就需要先知道, 服务器所在的位置.public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 这个 new 操作完成之后, 就完成了 tcp 连接的建⽴.socket = new Socket(serverIp, serverPort);}public void start() {System.out.println("客⼾端启动");Scanner scannerConsole = new Scanner(System.in);try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {while (true) {// 1. 从控制台输⼊字符串.System.out.print("-> ");String request = scannerConsole.next();// 2. 把请求发送给服务器PrintWriter printWriter = new PrintWriter(outputStream);// 使⽤ println 带上换⾏. 后续服务器读取请求, 就可以使⽤ scanner.nprintWriter.println(request);// 不要忘记 flush, 确保数据是真的发送出去了!!printWriter.flush();// 3. 从服务器读取响应.Scanner scannerNetwork = new Scanner(inputStream);String response = scannerNetwork.next();// 4. 把响应打印出来System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);client.start();}
}服务器引⼊多线程 
如果只是单个线程, ⽆法同时响应多个客⼾端. 
此处给每个客⼾端都分配⼀个线程.
// 启动服务器
public void start() throws IOException {System.out.println("服务器启动!");while (true) {Socket clientSocket = serverSocket.accept();Thread t = new Thread(() -> {processConnection(clientSocket);});t.start();}
}服务器引⼊线程池 
为了避免频繁创建销毁线程, 也可以引⼊线程池
// 启动服务器
public void start() throws IOException {System.out.println("服务器启动!");ExecutorService service = Executors.newCachedThreadPool();while (true) {Socket clientSocket = serverSocket.accept();// 使⽤线程池, 来解决上述问题service.submit(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});}
}⻓短连接
TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接: 
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发数据。 
⻓连接:不关闭连接,⼀直保持连接状态,双⽅不停的收发数据,即是⻓连接。也就是说,⻓连接可以多次收发数据。
对⽐以上⻓短连接,两者区别如下: 
• 建⽴连接、关闭连接的耗时:短连接每次请求、响应都需要建⽴连接,关闭连接;⽽⻓连接只需要第⼀次建⽴连接,之后的请求、响应都可以直接传输。相对来说建⽴连接,关闭连接也是要耗时 
的,⻓连接效率更⾼。 
• 主动发送请求不同:短连接⼀般是客⼾端主动向服务端发送请求;⽽⻓连接可以是客⼾端主动发送 请求,也可以是服务端主动发。 
• 两者的使⽤场景有不同:短连接适⽤于客⼾端请求频率不⾼的场景,如浏览⽹⻚等。⻓连接适⽤于客⼾端与服务端通信频繁的场景,如聊天室,实时游戏等。
基于BIO(同步阻塞IO)的⻓连接会⼀直占⽤系统资源。对于并发要求很⾼的服务端系统来说,这样的消耗是不能承受的。 
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在⼀个线程中运⾏。⼀次阻塞等待对应着⼀次请求、响应,不停处理也就是⻓连接的特性:⼀直不关闭连接,不停的处理请求。
实际应⽤时,服务端⼀般是基于NIO(即同步⾮阻塞IO)来实现⻓连接,性能可以极⼤的提升。
