1.UDP和TCP
UDP和TCP是传输层协议中最核心的两种协议
他们的特点分别是
UDP: 无连接,不可靠传输,面向数据报,全双工
TCP: 有连接,是可靠传输,面向字节流,全双工
有无连接
有连接:就好比两个人打电话,打电话的一方发出连接请求,被打电话的一方选择确认连接,此时双方才能进行通话
无连接:就好比QQ,微信发消息,发消息的一方不需要和另一方取得连接就可以发送消息
可靠传输
在网络通信中,是没办法保证100%传输成功的
不可靠传输:发送出去就不管了
可靠传输:发送方能知道信息是否传出去了
面向数据报/面向字节流
面向数据报:以数据报为单位进行接发
面向字节流:以字节为单位进行接发,类似于文件读写
全双工
一个通信通道,可以双向传输(既可以接收,也可以发送)
半双工:可以发也可以收,但是不可以同时进行
2.实现UDP版本的服务器和客户端
为方便,此处实现的服务器为回显服务器(服务器的响应和请求相同),TCP也是一样的
首先要认识几个类
2.1 DatagramSocket
使用此类,表示一个socket对象,有了一个socket对象就可以对另一台主机进行通信了
注意:在操作系统中,socket会当做文件来处理,会存放到文件描述符表中
构造方法:
1.DatagramSocket(int port)
里面传入一个端口号(1024~65535),可以确定端口
2.DatagramSocket()
操作系统会随机分配一个空闲的端口
方法:
void receive(DatagramPacket p)
用来接收数据报,此时要传入一个空的DatagramPacket对象,内容及数据会存放到DatagramPacket对象中
void send(DatagramPacket p)
用来发送数据报,数据要提前存放在DatagramPacket对象中
2.2 DatagramPacket
表示UDP中传输的一个报文,可以指定一些数据进去
构造方法:
DatagramPacket(byte[] buf,int length)
里面放一个byte数组作为缓冲区,length是要存放的长度
DatagramPacket(byte[] buf,int length,SocketAddress address)
和上面的相比,多了一个address参数,该参数可以传入IP和端口号
方法:
InetSddress getAddress()
获取IP地址
int getPort()
获取端口号
byte[] getData()
获取数据报中的数据
2.3 编写UDP版本的服务器
首先搭建好一个大体框架,并在构造方法中创建出一个DatagramSocket对象
注意:构造时要传入一个端口号,因为在网络通信中,服务器是被动方,如果端口号为随机,那么此时客户端就不知道服务器的端口号,也就无法进行通信了
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() {
}
}
然后在start中实现具体的服务器代码
首先是一个while(true)循环包含整个方法体,因为大多数服务器需要全天24h不断的去处理请求
之后大致分四步:
1.读取客户端发来的请求
//receive 方法的参数是一个输出型参数,需要构建好一个空白的 DatagramPacket 对象,让receive去补充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//如果客户端发来了请求,receive就能顺利读出来请求,如果没有请求就会进入阻塞
socket.receive(requestPacket);
//将数组中的数据转化为字符串,方便读取
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
2.根据请求,构建响应
String response = process(request);
process方法为处理响应的方法(就是将请求直接返回去)
public String process(String request) {
return request;
}
3.返回响应
//此处DatagramPacket 的参数就不能是空的数组了,因为要将响应的字符串返回去
//send的参数也是DatagramPacket
//在receive时,requestPacket中已经存放了 客户端的IP地址和端口号
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
4.打印信息(可有可无)
System.out.printf("[%s:%d] req: %s;resp : %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
打印内容为(从左到右): 客户端的IP,客户端的端口号,请求,响应
整体代码如下:
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],4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2. 根据请求制造响应
String response = process(request);
//3. 将响应写会到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
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 {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
2.4 编写UDP版本的客户端
构建框架,相比于服务器,客户端在构造方法处还需要传入服务器的端口号和IP
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP = null;
private int serverPort = 0;
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() {
}
}
start中的具体实现
仍然是一个while循环包含方法体
循环内容大致分四步:
1.从控制台构建请求
//该句在循环外,如果每次循环都去创建Scanner太耗效率了
Scanner scanner = new Scanner(System.in);
System.out.print(">");
String request = scanner.next();
//判断是否要退出
if(request.equals("exit")) {
System.out.println("goodbye");
break;
}
2.发送请求
// 构造一个 DatagramPacket 对象 此时该对象的参数要传入客户端的 IP(serverIP) 和 端口(serverPort)
// 此处的IP需要的是一个32的整数形式,需要使用 InetAddress.getByName 进行一个转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
3.接收响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
4.打印响应
System.out.println(response);
整体代码如下:
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP = null;
private int serverPort = 0;
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.print(">");
String request = scanner.next();
if(request.equals("exit")) {
System.out.println("goodbye");
break;
}
//2.构造成一个UDP请求,并发送
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);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.把解析好的结果显示出来
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
3.实现TCP版本的服务器和客户端
3.1 ServerSocket
ServerSocket是专门给服务器(TCP)使用的socket对象
构造方法:
ServerSocket(int port)
构建一个socket并指定端口号
方法:
Socket accept()
开始监听指定端口,当有客户端绑定时,返回一个Socket对象并建立连接,否则,则进入阻塞等待
3.2 Socket
既会给客户端提供服务,也会给服务器提供服务(TCP)
构造方法:
Socket(String host,int port)
构造一个Socket对象,并与传入的端口号和IP进行连接
方法:
InputStream getInputStream()
返回一个与该Socket连接相关连的InputStream 对象
OutputStream getOutputStream()
返回一个与该Socket相关连的OutputStream 对象
3.3 编写TCP版本的服务器
首先是一个大体的框架,将ServerSocket对象打包好
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() {
}
//处理连接
public void processConnection(Socket clientSocket) {
}
//处理请求
public String process(String request) {
return request;
}
}
在连接的处理时,首先要连接上资源(InputStream 读,OutputStream 写)
然后将以下四个步骤循环执行
1.读取请求
2.根据请求构造响应
3.返回响应
4.打印请求和响应(可有可无)
由于分段解释比较杂乱,此处将整个处理连接过程的代码全部给出
//处理连接
public void processConnection(Socket clientSocket) {
//打印日志 -- 客户端的IP和端口号
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()
) {
Scanner scanner = new Scanner(inputStream);
while(true) {
//1.读取请求
if (!scanner.hasNext()) {
//此时没有下一个数据(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求构造响应
String response = process(request);
//3.返回响应
//可以使用 OutputStream 将字符串转化为字节数组传入
//也可以使用字符流 PrintWrite 来转换
//将 OutputStream 对象放入 PrintWrite 的对象参数中 就可以通过 PrintWrite 来输出
PrintWriter printWriter = new PrintWriter(outputStream);
//此处将数据写入,使用println换行 方便对端接收解析
printWriter.println(response);
//flush 用来刷新缓冲区
printWriter.flush();
//4.打印请求和响应
System.out.printf("[%s:%d] req:%s resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
//由于每一次客户端进行连接时都会创建一个 clientSocket 出来
//而 Socket 属于文件 会占用文件描述符表
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在start中也需要一个循环去不断接收客户端的连接请求(此处使用的是单线程模式,如果上一个客户端的请求没有处理完,是无法处理下一个的)
//单线程版本
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
//使用 clientSocket 来和客户端交流
Socket clientSocket = serverSocket.accept();
//使用该方法处理连接
processConnection(clientSocket);
}
}
整体代码如下:
多线程版本和线程池版本的start一并放到下面的代码中
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) {
//创建线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//使用 clientSocket 来和客户端交流
Socket clientSocket = serverSocket.accept();
threadPool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}*/
//多线程版本
/*public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
//使用 clientSocket 来和客户端交流
Socket clientSocket = serverSocket.accept();
//使用多线程来处理
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}*/
//单线程版本
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
//使用 clientSocket 来和客户端交流
Socket clientSocket = serverSocket.accept();
//使用该方法处理连接
processConnection(clientSocket);
}
}
//处理连接
public void processConnection(Socket clientSocket) {
//打印日志 -- 客户端的IP和端口号
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()
) {
Scanner scanner = new Scanner(inputStream);
while(true) {
//1.读取请求
if (!scanner.hasNext()) {
//此时没有下一个数据(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求构造响应
String response = process(request);
//3.返回响应
//可以使用 OutputStream 将字符串转化为字节数组传入
//也可以使用字符流 PrintWrite 来转换
//将 OutputStream 对象放入 PrintWrite 的对象参数中 就可以通过 PrintWrite 来输出
PrintWriter printWriter = new PrintWriter(outputStream);
//此处将数据写入,使用println换行 方便对端接收解析
printWriter.println(response);
//flush 用来刷新缓冲区
printWriter.flush();
//4.打印请求和响应
System.out.printf("[%s:%d] req:%s resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
//由于每一次客户端进行连接时都会创建一个 clientSocket 出来
//而 Socket 属于文件 会占用文件描述符表
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
3.4 编写TCP版本的客户端
首先是大体框架
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//下面这个构造语句就会触发TCP建立连接
//里面需要传服务器的IP和端口号
socket = new Socket(serverIP,serverPort);
}
public void start() {
}
}
start中实现请求的传输和获取响应
首先是资源的创建(InputStream 读,OutputStream 写)
然后套入循环执行
分为四步(大部分和服务器相似):
1.获取请求
2.发送请求
3.接收响应
4.打印响应(可有可无)
public void start() {
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true) {
//1.从键盘获取用户请求
Scanner scanner = new Scanner(System.in);
System.out.print(">");
String request = scanner.next();
if(request.equals("exit")) {
System.out.println("goodbye");
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) {
e.printStackTrace();
}
}
整体代码如下:
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//下面这个构造语句就会触发TCP建立连接
//里面需要传服务器的IP和端口号
socket = new Socket(serverIP,serverPort);
}
public void start() {
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true) {
//1.从键盘获取用户请求
Scanner scanner = new Scanner(System.in);
System.out.print(">");
String request = scanner.next();
if(request.equals("exit")) {
System.out.println("goodbye");
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) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}