目录
socket(套接字)
程序猿在进行网络编程的时候,主要编写的是应用层的代码,在传输数据的时候,需要上层协议调用下层协议,应用层需要调用传输层的一些api,而这些api统称为socket api
(操作系统提供的api是C/C++风格的api,这些api由JVM封装过,在使用时,Java程序猿使用封装过的api)
操作系统为应用层提供的api由很多很多,其中最主要的有两组,分别对应传输层UDP的api和TCP的api
TCP与UDP区别:
UDP:无连接;不可靠传输;面向数据报;全双工
TCP:有连接;可靠传输;面向字节流;全双工
有无连接:
这里的连接是一个抽象概念,并不是像绳子一样连接
这里的连接指的是有无记录,如果传输双端互相记录了对方,就叫有链接
比如发送短线验证码只是根据人为输入的电话号码发送短信,服务器并没有记录你的电话号码。直接投送短信,这就是无连接
而打电话时必须先通过记录的双发信息让电话联通,才能通话,即必须先记录连接才能信息传输,如果连接无法建立就不能信息交互,这就是有连接
无连接:使用UDP通信的双方,不需要刻意保存对端的相关信息
有连接:使用TCP通信的双方,需要可以保存对方的相关信息
可靠传输与不可靠传输:
传输成功与否并不只是程序猿的责任,如果相关硬件设施出现问题,比如光纤断裂,照样传输失败
可靠传输:尽可能的传输,如果传输失败,是可以得知的
不可靠传输:只关心消息发没发送,不关心传输失败与否
面向数据报与面向字节流:
面向数据报:以一个UDP数据报为基本单位
面向字节流:以字节为传输的基本单位,读写非常灵活
全双工与半双工:
UDP的相关api:
DatagramSocket API:
Datagram就是数据报的意思,而Socket说明这个对象是一个特殊的文件,区别于普通文件与目录,socket文件不是对应在硬盘的某个文件中,而是网卡这个硬件设备中
无线网卡:
有线网卡(未连接):
想要进行网络通信,就需要socket文件这样的对象,借助socket文件对象,才能间接操控网卡
向socket对象中写数据,相当于通过网卡发送消息
从socket对象中读数据,相当于通过网卡接收消息
构造方法:
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
客户端一般使用无参数的版本,由操作系统自动分配即可
服务端一般使用带参数的版本,服务端的端口号必须不变,以便于客户端能找得到服务端
类似于(店面位置不可变,但顾客在饭店内坐哪桌是无所谓的)
其他方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
注意,使用完socket文件后需要关闭资源,不然会造成文件资源泄露
DatagramPacket API:
DatagramPacket是UDP Socket发送和接收的数据报
构造方法:
方法签名 | 方法说明 |
DatagramPacket(byte[]buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[]buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
第一个版本的构造方法不需要设置地址,通常用于接收消息
第二个版本的构造方法需要显式的设置地址进去,通常用于发送消息
其他方法:
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
使用UDP的api写一个回显服务器与客户端:
(注意运行的时候要先运行服务器再运行客户端,因为先运行服务器,服务器会因为receive方法阻塞等待客服端的请求,如果先运行客户端,在客户端发送请求的时候,服务器没有启动则会出现问题)
服务器:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//需要先定义一个socket对象,
//通过网络通信必须使用socket对象
private DatagramSocket socket = null;
//这里可能会抛出异常,并不是给了端口号,就一定能绑定成功,
//如果失败,其大概率原因是该端口已经被其他进程绑定了
//同一台主机,同一时刻,一个端口只能被一个进程绑定
public UdpEchoServer(int port) throws SocketException {
//让socket绑定/关联相关端口
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器并不是单独为某一个客户端只服务一次的
//可能由许多客户端,而每个客户端可能有许多请求
//所以服务器必须使用while(true)循环,时刻准备请求的到来
while (true){
//在每次循环内都需要做三件事
//1.读取请求并解析
//这里要先创建一个数据报
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//然后将这个空的数据报传入receive方法中,如果socket收到客户端发送的
//请求,那么在该方法内packet就会被得到填充,如果收不到客户端发来的请求
//即暂时无客户端需要发送请求,那么该方法就会阻塞等待,直到收到请求
//该方法涉及到IO操作,即在网卡中读写,所以可能会导致IO异常
socket.receive(requestPacket);
//为了方便解析使用请求,这里将请求变为一个字符串(该操作不是必须的,只是为了方便)
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算响应结果
String respond = process(request);
//3.把响应结果写回客户端内
//要先打包包裹
DatagramPacket respondPacket = new DatagramPacket(respond.getBytes(),respond.getBytes().length
//用于发送的包裹在初始化的时候需要获取客户端的地址
//可以通过接收的请求获取到地址,这个地址是一个SocketAddress对象
//该对象内部包含客户端的IP地址和相应的端口号
,requestPacket.getSocketAddress());
socket.send(requestPacket);
//打印日志
System.out.printf("[%s : %d] request: %s respond: %s\n",requestPacket.getAddress().toString()
,requestPacket.getPort(),request,respond);
}
}
private String process(String request) {
//这里用于计算响应结果
//这只是一个回显程序,就省略计算了
return request;
}
}
客户端:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Client {
private DatagramSocket socket = null;
private String severIP;
private int severPort;
private String name;
public Client(String severIP, int severPort,String name) throws SocketException {
this.severIP = severIP;
this.severPort = severPort;
this.name = name;
socket = new DatagramSocket();
}
public void start() throws IOException {
printMessageThread();
online();
Scanner scanner = new Scanner(System.in);
while (true){
String outcome = name + " " + scanner.nextLine();
DatagramPacket outcomePacket = new DatagramPacket(outcome.getBytes(),
outcome.getBytes().length, InetAddress.getByName(severIP),severPort);
socket.send(outcomePacket);
}
}
public void printMessageThread() {
Thread thread = new Thread(() -> {
while (true){
DatagramPacket incomePacket = new DatagramPacket(new byte[4096],4096);
try {
socket.receive(incomePacket);
} catch (IOException e) {
e.printStackTrace();
}
String income = new String(incomePacket.getData(),0,incomePacket.getLength());
String[] strings = income.split(" ",2);
String name = strings[0];
String message = strings[1];
System.out.println(name + ":" + "\n" + " " + message);
}
});
thread.start();
}
public void online() throws IOException {
String outcome = name + " " + name + "上线";
DatagramPacket outcomePacket = new DatagramPacket(outcome.getBytes(),
outcome.getBytes().length,InetAddress.getByName(severIP),severPort);
socket.send(outcomePacket);
}
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入昵称:");
String name = scanner.nextLine();
Client client = new Client("127.0.0.1",9090,name);
client.start();
}
}
这里的客户端与服务器内部的socket都没有调用close方法,因为这是一个简单的回显程序,内部的while(true)循环会一直执行,如果跳出了循环,即意味着start方法结束,然后main方法结束,main方法结束意味着进程结束,其会自动释放该程序占用的资源
也就是说,这里的socket的生命周期比较长,伴随整个进程,所以在整个进程结束的时候其资源会自动释放,而对于某些临时创建的DatagramSocket,其就需要手动close
如果相隔很远的两台电脑,一台启动服务器,一台启动客户端,是不能直接建立连接的,因为内网不能直接被访问,必须把客户端部署到云服务器上,拥有了外网IP,那么另一端的内网服务器才能访问到
TCP的相关api:
ServerSocket API:
主要给服务器使用
构造方法:
SeverSocket只辅助于服务器,所以必须指定端口号,就算在构造时未指定端口号,后续在使用的时候也需要指定端口号
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
其他方法:
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket API:
构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接(这里的host port分别代表服务器的ip和端口号) |
其他方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
void close() | 关闭此套接字 |
使用TCP的api写一个回显服务器与客户端:
服务器:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoSever {
//SeverSocket是专用于服务器的类
private ServerSocket serverSocket = null;
public TcpEchoSever(int port) throws IOException {
//专用于服务器,所以需要绑定端口号
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.printf("[%s : %d]服务器启动\n",
serverSocket.getInetAddress().toString(),
serverSocket.getLocalPort());
ExecutorService executorService = Executors.newCachedThreadPool();
while (true){
//每次服务器收到客户端的请求时,这个请求就需要用一个Socket类对象
//来接收,请求的内容包含在clientSocket内
//一个clientSocket内可能包含多个请求
//如果没有客户端连接,那么accept方法就会堵塞
Socket clientSocket = serverSocket.accept();
//由于processConnection方法内部也存在while(true)循环
//所以这里直接不能直接调用processConnection,因为如果直接
//调用,那么当前循环就会等待processConnection调用完成
//也就是说会导致当前服务器只服务于一个客户端
//正确方式应该是采用多线程的方式
// Thread thread = new Thread(() -> {
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// thread.start();
//当然我们也可以采用线程池的方式,采用线程池比频繁的创建销毁线程更加高效
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
//这里采用长连接的形式处理clientSocket,即认为clientSocket内会包含多个请求
try(InputStream inputStream = clientSocket.getInputStream();
//注意 这里的input 和output 都是从clientSocket获取的资源,
//也就是说这三部分其实是用一份资源,关闭其中之一,另外两个也会自动关闭
OutputStream outputStream = clientSocket.getOutputStream()){
PrintWriter printWriter = new PrintWriter(outputStream);
//为了方便读取请求,这里做出以下规定:
//每个请求时字符串
//请求与请求之间采用\n的形式分割
//这意味着我们在写入响应的时候也要遵守上述规定
//规定不唯一,根据习惯可以采取其他方式
Scanner scanner = new Scanner(inputStream);
System.out.printf("[%s : %d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
while (true){
if (!scanner.hasNext()){
//读到末尾了
System.out.printf("[%s : %d] 客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
String respond = process(request);
printWriter.println(respond);
//由于网卡的读写速度是非常慢的,所以一般是将数据写到缓冲区内
//等缓冲区满了的时候再将所有的数据一次性打包写到网卡内
//也就是说其实上面的println并不是直接写到网卡内,而是写到了缓冲区
//但是我们这里等不到缓冲区满,所以我们采取手动刷新缓冲区的方式
//flush强行让未满的缓冲区进行一次IO操作
printWriter.flush();
System.out.printf("[%s : %d] request: %s respond: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,respond);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//clientSocket只是给某一次连接进行服务的
//在这次连接使用完毕的时候,是需要释放资源的
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoSever tcpEchoSever = new TcpEchoSever(9090);
tcpEchoSever.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;
public TcpEchoClient(String severIp,int port) throws IOException {
//这一步操作相当于让服务器与客户端建立TCP连接
//连接建立成功后,服务器的accept方法就会取消阻塞等待
this.socket = new Socket(severIp,port);
}
public void start(){
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromSocket = new Scanner(inputStream);
while (true){
//1.从键盘上读取用户输入的内容
System.out.print("->");
String request = scanner.next();
//2.根据读取内容构造请求,发送给服务器
printWriter.println(request);
printWriter.flush();
//3.读取响应
String respond = scannerFromSocket.next();
System.out.printf("request: %s respond: %s\n",request,respond);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}