Socket
Socket是基于TCP/IP协议的网络通信的基本操作单元;
Socket是操作系统给应用程序提供的一组API,充当了应用层和传输层的桥梁.
传输层中两类核心协议:UDP TCP
UDP:无连接 不可靠传输 面向数据报 全双工
TCP:有连接 可靠传输 面向字节流 全双工
什么是有/无连接?
有连接:就像是我们打电话,拨打电话后要等待对方接通后,我们才可以交流
无连接:就像是我们发qq消息,我只关心我发送了这个消息,不关心你收没收到这个消息
我们发了消息就是发了 在网络中也难免存在网络波动导致这个信息可能有所丢失,所以也导致接收方压根没接收到这个信息
什么是可/不可靠传输?
可靠传输:明确知道对方是否收到消息;就像我们依旧在qq发信息,当我们把信息发送出去,等待对方确认回应是否收到信息;如果一段时间没有应答,那我们将继续重新发送消息(超时重传)以便对方确认收到信息.
简单来说:传输过程中,发送方知道接收方有没有接收到信息.
但是对于可靠传输,只能保证我100%确定这个消息被对方接收,并不代表这个传输就是安全传输,不能保证安全性.
不可靠传输:传输过程中,发送方不知道接收方有没有接收到信息.
对于UDP、TCP(有舍有得)
UDP是不可靠传输,但传输效率更高.
TCP是可靠传输,但传输效率降低了.
对应上述结论:UDP是不可靠传输,TCP是可靠传输;所以TCP比UDP更安全?大错特错!
面向字节流/面向数据报
TCP和文件操作特别类似 都是以“流”的方式(在这里传输单位是字节,称作字节流~)
UDP是面向数据报 读写单位是一个UDP数据报(包含了一系列的数据/属性 后期会写的~)
全双工/半双工
全双工:一个通道,可以双向通信
半双工:一个通道,只能单向通信
UDP
先认识UDP的socket api 两个核心类:DatagramSocket、DatagramPacket
DatagramSocket
是一个socket对象 要进行网络通信必须得先有socket对象
DatagramPacket
表示一个UDP数据报 代表了系统中设定的UDP数据报的二进制结构
UDP客户端服务器
写网络程序的代码时,经常会遇到下面这种异常
上述最典型的情况:端口被占用
端口号主要区分主机上的应用程序;一个应用程序可以占据主机上的多个端口,一个端口只能被一个进程占用(排除特例~)
当端口已经被别的进程占用,你再尝试创建这个socket对象,占用该端口,此时就肯定会报错啦~
下述代码:
1.读取客户端发来的请求
2.根据请求计算机响应
3.把响应写回给客户端
4.站在服务器的角度:
源IP:本机IP;源端口:指定的端口 (服务器绑定的端口)
目的IP:包含在收到的数据报中;目的端口:包含在收到的数据报中协议类型 UDP
//Udp 的回显服务器
//客户端发送的请求是啥,服务器返回的就是啥
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);
//记录日志 方便查看
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();
}
}
为啥子服务器一上来就接收信息 而不是发送呢?
一个服务器要给需要客户端提供服务,服务器也不知道客户端啥时候来,只能随时准备着~
被动的一方:服务器;主动的一方:客户端
创建客户端,向服务器发送数据并且获取服务器返回的数据,并且打印到屏幕
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();
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true) {
//1.从控制台读取用户输入
System.out.printf("->");//命令提示符
String request = scanner.next();
//2.构造请求对象 并发给服务器
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 static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
1.先在控制台读取用户输入的数据
2.把用户输入的内容 构建成一个UDP请求而发送
3.从服务器读取响应数据并解析
4.把响应结果打印到到控制台
5.在客户端角度:
源IP:本机IP;源端口:系统分配的端口
目的IP:服务器的IP;目的端口:服务器的端口
协议类型:UDP
启动上述代码并输入~
那么我们可以开启多个客户端吗?可以!
这样就可以开启多个服务端~ 同样可以运行~
TCP
分量比UDP更重 用的更多的协议
TCP提供的api也是主要的两个类
ServerSocket:给服务器使用的socket
Socket:即会给服务器用,也会给客户端使用 面向字节流,一个字节一个字节进行传输
一个TCP数据报就是一个字节数组 byte[]
TCP回显服务器
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 此处不应该创建固定线程数目的线程池.
private ExecutorService service = Executors.newCachedThreadPool();
// 这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 这个写法, 是能自动关闭, 也行. 实现 Closeable 接口就可以这么写.
// 这么写会有其他问题. (结合后面讲第二个问题, 再说这个事)
Socket clientSocket = serverSocket.accept();
// 使用线程池, 来解决上述问题
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
// 单个线程, 不太方便完成这里的一边拉客, 一边介绍. 就需要多搞线程. 主线程专门负责拉客. 每次有一个客户端, 都创建一个新的线程去服务
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
}
}
// 通过这个方法来处理一个连接的逻辑.
private void processConnection(Socket clientSocket) {
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 scanner = new Scanner(inputStream);
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 使用 PrinterWriter 包裹一下, 方便进行发数据.
PrintWriter writer = new PrintWriter(outputStream);
// 使用 PrintWriter 的 println 方法, 把响应返回给客户端.
// 此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
writer.println(response);
// 这里还需要加一个 "刷新缓冲区" 操作.
writer.flush();
// 日志, 打印当前的请求详情.
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, 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();
}
}
编写客户端
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.next 来获取了
printWriter.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();
}
}