目录
1.什么是网络编程
2.TCP/IP协议
3.Socket套接字
流套接字:使用传输层TCP(传输控制协议)
数据报套接字:使用传输层UDP(用户数据报协议)
原始套接字
4.Java数据报套接字通信模型
UDP数据报套接字编程
DatagramSocket API
DatagramPacket API
InetSocketAddress API:
示例一:UDP版本回显服务器
示例二:"查词典"服务器
5.Java流套接字通信模型编辑
TCP流套接字编程
ServerSocket API
Socket API
tcp中的长短连接
示例三:TCP版本回显服务器
6.TCP流套接字编程与数据报套接字编程对比
1.什么是网络编程
网络编程是指网络上的主机,通过不同的进程,以编程的方式,实现网络通信(网络数据传输)
最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。中间最主要的就是数据包数据包数据包的组装,数据包的过滤,数据包的捕获,数据包的分析
在网络上通信的双方只有遵守相同的协议,才能正确地交流信息,就像人们交谈时要使用同一种语言一样,如果谈话里使用不同的语言,就会造成双方都不知所云,交流就被迫中断。网络通信最常见的就是TCP/IP协议
2.TCP/IP协议
TCP/IP协议是Internet最基本的协议,其中应用层的主要协议有Telnet、FTP、SMTP等,是用来接收来自传输层的数据或者按不同应用要求与方式将数据传输至传输层;传输层的主要协议有UDP、TCP,是使用者使用平台和计算机信息网内部数据结合的通道,可以实现数据传输与数据共享;网络层的主要协议有ICMP、IP、IGMP,主要负责网络中数据包的传送等;而网络访问层,也叫网络接口层或数据链路层,主要协议有ARP、RARP,主要功能是提供链路管理错误检测、对不同通信媒介有关信息细节问题进行有效处理等
3.Socket套接字
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基 于Socket套接字的网络程序开发就是网络编程
基于操作系统提供的 socket api 来进行 网络数据 的发送和接收
socket api是和传输层密切相关的,socket api针对传输层协议划分了三种:
流套接字:使用传输层TCP(传输控制协议)
传输控制协议定义了两台计算机之间进行可靠的传输而交换的数据和确认信息的格式,以及计算机为了确保数据的正确到达而采取的措施。协议规定了TCP软件怎样识别给定计算机上的多个目的进程如何对分组重复这类差错进行恢复。协议还规定了两台计算机如何初始化一个TCP数据流传输以及如何结束这一传输。TCP最大的特点就是提供的是面向连接、可靠的字节流服务。
TCP特点:
有连接,可靠传输,面向字节流,有接收缓冲区和发送缓冲区,传输大小不受限制
数据报套接字:使用传输层UDP(用户数据报协议)
用户数据报协议是一个简单的面向数据报的传输层协议。提供的是非面向连接的、不可靠的数据流传输。UDP不提供可靠性,也不提供报文到达确认、排序以及流量控制等功能。它只是把应用程序传给IP层IP层IP层的数据报发送出去,但是并不能保证它们能到达目的地。因此报文可能会丢失、重复以及乱序等。但由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
UDP特点:
无连接,不可靠传输,面向数据报,有接收缓冲区,无发送缓冲区 ,传输受限制:最多一次64k
面向数据报,传输数据是一块一块的,发送一块100个字节的数据,必须一次性发送,接收也是必须一次接收100个字节,不能分100次,一次1字节..
原始套接字
用于自定义传输层协议,用于读写内核没有处理的IP协议数据,简单了解就行
4.Java数据报套接字通信模型
对于UDP协议来说,具有无连接面向数据报的特征,每次通信都不建立连接,并且一次发送所有数据报,一次接收所有数据报,Java中使用UDP通信协议,主要基于DatagramSocket类来创建数据报套接字,并且使用DatagramPacket作为发送或接受的UDP数据包
对于一次发送及接收UDP数据报的流程如下:
上图只是一次发送端的UDP数据报发送,接收端的数据报接收,并没有返回的数据。
也就是只有请求没有响应
对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
学习了数据报套接字通信模型,就可以基于模型进行网络编程了,下来学习UDP数据报套接字编程
UDP数据报套接字编程
DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报
看看它的构造方法
DatagramSocket():创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端)
DatagramSocket(int port):创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用 于服务端)
为什么客户端可以系统随机分配端口号,服务器必须指定端口号呢?
对于客户端来说,端口可以是系统分配的,如果手动指定端口号,可能会和客户端其他程序产生端口冲突,(服务器不怕冲突,程序是可以掌控的,客户端程序运行在用户电脑上,环境复杂不可控)
对于服务器来说必须要在创建socket对象的时候给绑定一个 具体的端口号 也就是说服务器一定要绑定一个具体的端口!!! 服务器是网络传输中被动的一方,如果是操作系统随机分配 的端口号,那么客户端就不知道这个端口号是什么了,就会导致无法通信!
DatagramSocket 方法:
void receive(DatagramPacket p):从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p):从此套接字发送数据报报(不会阻塞等待,直接发送)
void close():关闭此数据报套接字
DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报
DatagramPacket 构造方法:
DatagramPacket(byte[] buf, int length):构造一个DatagramPacket以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address):
构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数 length)。address指定目的主机的IP和端口号
DatagramPacket 方法:
InetAddress getAddress():从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址
int getPort():从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获 取接收端主机端口号
byte[] getData():获取数据报中的数据
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创 建。
InetSocketAddress API:
InetSocketAddress ( SocketAddress 的子类 )构造方法:
InetSocketAddress(InetAddress addr, int port) :创建一个Socket地址,包含IP地址和端口号
示例一:UDP版本回显服务器
根据上述所学,我们写一个一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只 有客户端请求,但没有服务端响应的示例:
UDP服务端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP版本的回显服务器
public class UdpEchoServer {
/*
网络编程本质上是操作网卡
但是网卡不方便直接操作,在操作系统内核中使用了特殊的文件"socket"
来抽象地表示网卡
因此进行网络通信,势必需要创造一个socket对象
*/
private DatagramSocket socket = null;
/*
对于服务器来说必须要在创建socket对象的时候给绑定一个
具体的端口号
也就是说服务器一定要绑定一个具体的端口!!!
服务器是网络传输中被动的一方,如果是操作系统随机分配
的端口号,那么客户端就不知道这个端口号是什么了,就会导致无法通信!
*/
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!!");
//要注意服务器是不止给一个客户端服务的
//while会快速循环
while(true){
//这里的死循环是必须的,因为服务器就是要一直运行,等待客户端的请求,除了个别服务器,像12306
//每天都会定时维护
//只要有客户端过来就可以提供服务
//1.读取客户端的请求
//receive方法参数是输出型参数.
//输出型参数.不是我们传数据然后方法使用,而是我i们传进去一个空壳让方法填充数据再返回
//receive方法接收请求需要先构造一个空白的DatagramPacket对象,然后交给receive来填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//客户端发请求了receive就顺利读出,如果没有发送请求,此时receive就阻塞,类似与Scanner读取控制台的操作
//客户端请求太多处理不过来也就是"高并发"
//处理高并发:多线程/多加机器(管理成本提高--分布式)
socket.receive(requestPacket);
//receive 内部会针对参数对象填充数据.
//填充的数据来自于网卡,从网卡读完,填充到对象中
//此时requestPacket中包含的数据不方便直接处理,是一个特殊的对象,我们将它转换成字符串就好处理了
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//requestPacket.getLength()是获取到实际的数据的长度,避免浪费
//可能这个数据报转换成字符串后长度很小,我们就只构造相应的长度,而不是构造byte[]数组那么长的字符串
//2.根据请求,计算响应,这里是回显服务器,响应和请求相同
String response = process(request);
//3.把响应写回客户端
//使用send方法,参数也是DatagramPacket,需要先构造好对象
//响应对象要使用响应数据来构造,不能是空的
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,requestPacket.getSocketAddress());
/*
此处的长度使用response.length()和response.getBytes().length
有什么区别呢??
两个写法计量单位不同,一个是字节的个数,一个是字符的个数,如果存的数据都是ASCII的数据就没差别
但是如果存的汉字,那结果就大相径庭
DatagramPacket是按字节来处理的,所以我们要计算的是响应数据的字节个数
___________________
requestPacket.getSocketAddress()是客户端的IP和端口号
*/
//发送
socket.send(responsePacket);
//4.打印中间结果
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//计算响应
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//端口号可以随意指定
//范围:1024-65535
UdpEchoServer server = new UdpEchoServer(1090);
server.start();
/*
理解服务器的工作流程
1.读取请求并解析
2.根据请求计算响应
3.构造响应并写回给客户端
*/
}
}
启动程序后,此时代码阻塞等待在 socket.receive(packet) 代码行,直到接收到一个UDP数据报
UDP客户端
import javax.security.auth.login.CredentialException;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/*
服务器的端口是固定指定的,为了方便用户端找到服务器程序
客户端的端口是系统分配的,如果手动指定可能会有客户端的其他程序端口冲突
服务器不怕冲突,因为服务器上面的程序可控,但是客户端的程序是运行在客户电脑上的,环境是复杂的,不可控
服务器要在构造方法中指定好端口
*/
public class UdpEchoCLient {
public DatagramSocket socket = null;
//构造这个对象不需要显式的绑定一个端口,让操作系统自动分配端口(随机挑选一个空闲的)
//对于服务器:端口必须是确定好的
//对于客户端来说,端口可以是系统分配的
//一次通信涉及到的IP和端口:
//端口号用来标识/区分一个进程,因此不允许一个端口同时被多个进程使用(同一个主机上)
//源IP和目的IP,yuan端口和目的端口
//发送方为源,接收方为目的
//由于客户端和服务器都在一个主机上,IP都是127.0.0.1(环回IP),端口是指定了的
private String serverIP = null;
private int serverPort = 0;
//IP是已知的127.0.0.1
// port是自动分配的
//服务器的IP和端口号也要告诉客户端,才能顺利把消息发给服务器
public UdpEchoCLient(String serverIP, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
while(true){
//1.从控制台读取数据
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("byebye");
break;
}
//2.构造成UDP请求并发送
//传入的serverIP是一个字符串,点分十进制的,而IP地址需要传入一个32位的整数形式,
// InetAddress.getByName进行转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.读取服务器的UDP响应,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
//4.把解析好的结果展示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoCLient cLient = new UdpEchoCLient("127.0.0.1",1090);
cLient.start();
}
}
客户端启动,我们输入hello的字符串到服务端,在服务端接收后,控制台输出内容如下:
服务器端:
发送和接受的数据是相同的,因为我们写的回显服务器,process方法没有进行别的处理,直接返回了请求数据
修改配置后,可以允许多个实例同时访问服务器,也就是能接受多个请求
注意:
端口号用来标识/区分一个进程,因此不允许一个端口同时被多个进程使用
如果一个端口号在一台主机上同时被使用了,就会发生"端口冲突"
示例二:"查词典"服务器
回显服务器缺少业务逻辑,我们在上述代码上调整一个"查词典"的服务器(英译汉)
客户端代码不变,然后新建一个类继承回显服务器服务端后重写process方法
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoserver2{
private Map<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("hello","你好");
dict.put("world","世界");
dict.put("big","大");
dict.put("deprive","剥夺剥削");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"没有查到!");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDicyServer = new UdpDictServer(1090);
udpDicyServer.start();
}
}
启动服务器和客户端,输入英文
5.Java流套接字通信模型
TCP流套接字编程
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API
ServerSocket 构造方法:
ServerSocket(int port):创建一个服务端流套接字Socket,并绑定到指定端口
ServerSocket 方法:
Socket accept():开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close():关闭此套接字
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据 的
Socket 构造方法:
Socket(String host, int port):创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接
Socket 方法:
InetAddress getInetAddress():返回套接字所连接的地址
InputStream getInputStream():返回此套接字的输入流
OutputStream getOutputStream():返回此套接字的输出流
tcp中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。下次再发送,就重新建立连接
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
长短连接的区别:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
我们写一个TCP版本的回显服务器
示例三:TCP版本回显服务器
服务器:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
private TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
/*接收链接,前提是得有客户端来建立连接
客户端构造Socket对象时,就会指定服务器的IP和端口号
没有客户端连接,accept就会阻塞
使用socketClient和具体的客户端进行交流
Tcp socket里面涉及到两种socket对象serverSocket,socketClient
*/
Socket clientsocket = serverSocket.accept();
//每次客户端传过来就会创建一个clientsocket对象,也就是一个文件,就会占用一个文件描述符表的表项,
// 因此使用完毕之后,就需要进行资源的释放,下线之后关闭,放到finally中保证执行!!
processConnection(clientsocket);
}
}
//使用这个方法处理一个连接
//这一个连接对应到一个客户端,但是这里可能会涉及到多次交互
private void processConnection(Socket clientsocket) throws IOException {
System.out.printf("[%s:%d]客户端上线!\n",clientsocket.getInetAddress().toString(),clientsocket.getPort());
//基于上述socket对像和客户端进行通信
try(InputStream inputStream = clientsocket.getInputStream();
OutputStream outputStream = clientsocket.getOutputStream()) {
//处理多个请求响应,使用循环进行
while(true){
//1.读取请求
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//没有下个数据,读取完毕
System.out.printf("[%s:%d]客户端下线\n",clientsocket.getInetAddress().toString(),clientsocket.getPort());
break;
}
//此处使用next是一直读到换行符,空白符,空格结束,最终结果不包含上述空白符
String request = scanner.next();
//2.根据请求构造响应结果
String response = process(request);
//3.返回响应结果
//outputStream没有写字符串的方法,可以把String里的字节数组拿出来进行写入,
// 或者用字符流转换一下
PrintWriter printWriter = new PrintWriter(outputStream);
//此处使用 println写入,有个\n,方便对端接受解析
printWriter.println(response);
//flush用来刷新缓冲区,保证写入数据确实发送出去了
printWriter.flush();
System.out.printf("[%s%d] req: %s,res: %s\n",clientsocket.getInetAddress().toString(),clientsocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
finally {
clientsocket.close();
};
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
客户端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
//这里的socket和服务器的socket不是同一个对象,可以理解为电话的两端
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//Socket构造方法能识别点分十进制的ip地址,比DatagramPacket方便
//new对象时就会建立连接!!
socket = new Socket(serverIP,serverPort);
}
public void start(){
System.out.println("客户端启动!!");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while(true){
//1.键盘读取用户输入的内容
System.out.println(">");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("byebye");
break;
}
//2.构造请求发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3.读取服务器响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4.解析显示响应
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
启动服务器后,阻塞等待,然后启动客户端发送数据
注意:
我们把printWriter.println(request)的println换成print之后,就无法正常运行了
TCP协议是面向字节流的协议,字节流的特性就是一次传输多少字节都行.那么接收方如何知道一次读了多少字节呢?
这就需要在传输中明确的约定:此处代码中,.隐式约定了使用\n作为当前代码的请求/响应分割约定
比如客户端发送,服务器接收
next()会一直读到空白符才结束,如果发送方发送的数据中没有带有空白符,next就不会遇到空白符,就会一直等待,也不知道请求发完了没.但是没有新的请求,他还会等.所以这里需要一个"\n",所以再发送的时候就要给一个换行符这样的空白符.服务器发送,客户端接收也是同理
在输入的时候enter已经有一个换行符了,但是返回的结果不包括你按下的换行符..
输入hello\n=>request只是hello..
当前代码还有个很重要的问题,当前的服务器只能处理一个客户端的连接(也就是只能给一个客户端提供服务)
我们修改设置,能允许多个实例时,多启动一个客户端
只有一个客户端服务器上线了
客户端1能正常处理,客户端2不能
客户端1退出后,服务器提示下线,紧接着客户端2就上线了并处理!!
当有客户端连接服务器时,服务器的processConnection方法的循环就开始了,那么循环不结束,
processConnection方法就结束不了..accept就无法再次被调用到
那么如何让accept被多次调用到呢?
使用多线程,主线程专门进行accept,每次收到一个链接就创建一个新的线程,由这个新的线程负责处理这个新的客户端.
每个线程是独立的执行流,每个独立的执行流只能执行各自的逻辑,并发关系,不会因为阻塞就影响到别的线程执行
可以同时多个客户端来发送请求了!
但是如果很多客户端,就要频繁的创建线程释放线程,那我们可以用线程池!
这样就没问题了..
此处代码是长连接的方式,将两端的while循环都去掉就是短链接了!
短链接也得用多线程,因为请求什么时候发是不确定的,还是会产生阻塞,会导致processConnection没有立即返回,那么accept就不能再次执行.
虽然这里使用了线程池,但是还不够,如果客户端非常多,而且迟迟不断开,那么对于机器来说就是很大的负担.
多开服务器能解决这个问题,但是成本增加了!
是否有办法解决单机支持更大量客户端的问题?? C10M问题(单机处理1kw个...表示比C10K能多处理很多)
针对上述多线程版本的代码,最大的问题就是机器承担不了这么大的线程开销.是否有办法一个线程处理多个客户端连接呢?
操作系统提出了IO多路复用/转接
IO多路复用其实就像我们充分利用时间,在买饭的时间空隙去取快递..
在服务器这里,让一个线程处理多个客户端,给线程安排个集合,这个集合就放了一堆连接,这个线程就负责监听这个集合,哪个连接有数据来了,线程就处理哪个连接..这里应用了一个事实:虽然连接有很多,但是这些连接的请求并非严格意义的同时,还是有先后的
操作系统里提供了一些原生API,select,poll,epoll
在Java中,提供了一组NIO这样的类,封装了上述多路复用的API..
6.TCP流套接字编程与数据报套接字编程对比
UDP:无连接,不可靠传输,面向数据报,全双工
无连接的,直接收发数据
面向数据报
全双工
一个socket既可以发数据,也能接收数据
TCP:有连接,可靠传输,面向字节流,全双工
有连接
面向字节流,全双工
可靠传输
隐藏在TCP背后,从代码的角度感受不到,TCP诞生的意义就是为了解决可靠传输的问题的,后续详述
网络编程套接字内容就结束了.