网络编程套接字
研究网络编程套接字就是研究如何写代码完成网络编程
socket套接字是操作系统给应用程序提供的API
应用层和传输层是可以进行交互的,socket就是传输层给应用层提供的
在网络编程中, 有很多 的协议 ,最知名的协议就是TCP和UDP协议
这两种协议的工作特性差别比较大, 因此操作系统提供了两个版本的API
TCP和UDP的区别
TCP
- 有连接
- 可靠传输
- 面向字节流
- 全双工
UDP
- 无连接
- 不可靠传输
- 面向数据报
- 全双工
所谓的有连接就像是打电话, 只有对方接了电话,才能进行通信
只有双方建立好连接才能进行交互数据
可靠传输并不是说A 给B发消息,消息就100%会发送过去,因为网络环境十分的复杂,没办法保证100%能送到
可靠传输的意思是A至少能知道B有没有收到消息
TCP是面向字节流, 和文件操作是一样的,都是流的形式
UDP是面向数据报,基本单位是数据报
全双工相对的词是半双工
全双工 : 一个通道,双向通信
半双工 : 一个通道, 单向通信
UDP的套接字
UDP中需要掌握的类
- DatagramSocket
- DatagramPacket
文件操作: 先打开文件 然后读/写文件, 最后关闭文件
事实上,socket本质上也是一种文件
狭义的文件是存储在磁盘上的文件
广义的文件 : 操作系统把各种硬件设备和软件资源都抽象成了文件, 统一按照文件的方式进行管理
socket对应到网卡这个硬件设备,操作系统也是把网卡当做文件来管理的
通过网卡发送数据就是写文件
通过网卡接收数据就是读文件
所以DatagramSocket就是网卡的代言人
DatagramPacket代表的是一个UDP的数据报,也就是一次发送和接收的基本单位
接下来写一个UDP版本的客户端 服务器 程序的:回显服务器(客户端发什么,服务器就返回什么)
一般正经的服务器有一个很重要的环节 : 根据请求,计算出响应
但是这个回显服务器,主要是用来练练手的
UDP的服务器代码详解
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoSever {
private DatagramSocket socket = null;
//写一下构造方法
//这里形参是端口号,通常情况下,一个端口号代表一个进程,所以要想通过端口号找到进程,就要先绑定端口号
//参数的端口表示服务器要绑定的端口
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//使用start启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//由于不知道客户端什么时候发送请求,所以服务器要一直工作,所以使用死循环
while(true){
//目标: 在循环里处理一次请求
//1. 读取请求并解析--使用socket的receive方法
//构造DatagramPacket空对象的时候,也是要求要分配内存空间的
//也就是说,客户端发来请求,我先创建requestPacket,之后再装进requestPacket
DatagramPacket requestPacket = new DatagramPacket(new byte[2048],2048);
socket.receive(requestPacket);//这里的参数要求是DatagramPacket类型的,所以要先构造一个DatagramPacket的空的对象
//注意: receive的参数是输出型参数,也就是说传入一个空的对象,之后要将从网卡中读到的对象重新传入到这个对象中
//将请求转换成字符串,方便打印,new String可以传入一个字节数组,使之变成一个字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//2. 根据请求计算响应
String response = Response(request);
//3. 把响应写回到客户端--使用socket的send方法
//下面这一步其实与上面的requestPacket的构造是一样的,只是这里是具体的字节数组和长度,但是由于我要发送给客户端,所以我要知道客户端的地址,
//客户端的地址其实就包含在requestPacket中,所以还要加上 requestPacket.getSocketAddress(),之后再进行发送
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length(),
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志,记录当前的情况
//先打印出客户端的IP地址和客户端的端口号,最后是输入和输出的内容
System.out.printf("[%s %d] request: %s response: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//由于是回显服务器,所以传入什么就返回什么就行了
public String Response(String request) {
return request;
}
//创建main方法
public static void main(String[] args) throws IOException {
UdpEchoSever sever = new UdpEchoSever(9090);//调用构造方法
sever.start();
}
}
UDP的客户端代码详解
package UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIp;
private int severPort;
//构造方法是不需要写返回值的
public UdpEchoClient(String severIp, int severPort) throws SocketException {
//在服务器这里是要求指定一个端口号的
//但是, 在客户端这里并不是没有端口号,而是由系统自动分配一个空闲的端口号
//要是我们指定一个端口,万一用户此时的端口已经被占用了呢?所以在客户端上,不指定端口号,由系统自动分配
socket = new DatagramSocket();
//假设这里的IP地址是以1.2.3.4这样的点分十进制格式
this.severIp = severIp;
this.severPort = severPort;
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while(true){
//1.从控制台输入请求
System.out.println("-->");
String request = sc.nextLine();
//2.构造一个UDP请求,发送给服务器
//getBytes()是将字符串转换成字节数组,后面之所以不用request.length(),是因为request.length()单位是字符
//request.getBytes().length的单位是字节.
//既然要发送就要说明服务器的位置,所以后面还有加上IP和port
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.severIp),this.severPort);
socket.send(requestPacket);
//3.从服务器中读取UDP响应数据,并解析
//先创建一个新的空的responsePacket,之后在将读到的数据写进去
DatagramPacket responsePacket = new DatagramPacket(new byte[2048],2048);
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);//这里是目标IP和目标端口号
client.start();
}
}
最后的运行结果 :
简洁版
服务器
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoSever {
private DatagramSocket socket = null;
//端口参数表示服务器要绑定的端口
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
//1. 读取请求并解析--使用socket的receive方法
DatagramPacket requestPacket = new DatagramPacket(new byte[2048],2048);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//2. 根据请求计算响应
String response = Response(request);
//3. 把响应写回到客户端--使用socket的send方法
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志,记录当前的情况
System.out.printf("[%s %d] request: %s response: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
public String Response(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoSever sever = new UdpEchoSever(9090);//调用构造方法
sever.start();
}
}
客户端:
package UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIp;
private int severPort;
public UdpEchoClient(String severIp, int severPort) throws SocketException {
socket = new DatagramSocket();//自动分配端口号
this.severIp = severIp;
this.severPort = severPort;
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while(true){
//1.从控制台输入请求
System.out.println("-->");
String request = sc.nextLine();//遇到空格不会停止
//2.构造一个UDP请求,发送给服务器--使用socket.send()
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.severIp),this.severPort);
socket.send(requestPacket);
//3.从服务器中读取UDP响应数据,并解析--使用socket.receive()
DatagramPacket responsePacket = new DatagramPacket(new byte[2048],2048);
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);//这里是目标IP和目标端口号
client.start();
}
}
服务器的构造方法 : 只有一个port
客户端的构造方法: IP地址和端口号都要输入
主要的原因就是服务器的IP地址就是本机的IP地址,一般来说, 是没有必要输入的
但是,客户端有必要输入一下是哪一台服务器的IP地址和端口号,也就是目标IP地址和目标端口号
端口号
端口号是一个十六位的整数, 范围是0-65535,但是一般都是是使用1024-65535,因为0-1023都是已经被一些知名的应用程序占用了
客户端和服务器的调用过程:
对于服务器来说 ,这三个步骤: 读取请求并解析, 根据请求计算响应, 把响应写回客户端,这些步骤极快,所以即使有多个客户端发来请求,服务器也是可以响应的,本质上还是串行处理
要是计算的速度比较慢就要使用到多线程了,还是不行只能使用分布式了
首先启动服务器,使用就 jconsole 就能发现在服务器程序的第30行阻塞了,也就是socket.receive()处阻塞了
一台服务器要能够给多个客户端提供服务,如何在IDEA中启动多个客户端来运行程序呢?
这样子就能创建多个客户端了
以上的回显服务器只是练练手的,并没有什么实际意义
下面写一个查询单词服务器
继承一下UdpEchoServer,只要修改一下根据请求计算响应的具体内容就行了
客户端是不用动的,是通用的
package UDP;//一定要把包导进去
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
//写一个翻译的服务器-客户端
//直接使用继承,start一样的,只要修改一下计算响应的方法就行了
//所以的翻译就是使用哈希表key->value 对应的关系
public class UdpTranslateServer extends UdpEchoServer{
HashMap<String, String> dict = new HashMap<>();
public UdpTranslateServer(int port) throws SocketException {
super(port);
dict.put("falsh","闪电");
dict.put("dog", "狗");
dict.put("cat","猫");
}
@Override
public String Response(String request) {
//输入key,要是在哈希表中有这个key,就返回对应的value,要是没有,就返回后面的话
return dict.getOrDefault(request,"查不到这个单词");
}
public static void main(String[] args) throws IOException {
UdpTranslateServer server = new UdpTranslateServer(3030);
server.start();
}
}
一个服务器程序的基本流程是和上面一样的,但是最核心的就是"根据请求计算响应" => 服务器的业务逻辑
TCP 的套接字
TCP主要也是要回两个类
ServerSocket
Socket
下面是具体的类
SeverSocket类 :
Socket类:
服务器程序
package 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 listenSocket = null;
private TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//
while(true){
//1.调用accept接收客户端的连接
//listenSocket就像是拉客的,拉完之后交给clientSocket来进行一对一处理
Socket clientSocket = listenSocket.accept();
//2.处理这个连接
processConnection(clientSocket);
}
}
private static void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s %d] 客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//处理客户端请求
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] 客户端下线",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.将响应写回客户端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,确保服务器真的发回给客户端了
printWriter.flush();
System.out.printf("[%s %d] req: %s res: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//为什么只有clientSocket最后要被释放?前面的listenSocket不用手动释放?
//一个进程中能打开的文件是有限的,也就是说文件描述符表是有限的
//listenSocket在TCP服务器中是唯一的对象,不会占满文件描述符表,进程退出,会自动销毁
//clientSocket在循环中,每一个客户端都要分配一个,会被反复创建实例,每创建一个就要消耗一个文件描述符表,所以要及时销毁
clientSocket.close();//最后要将clientSocket销毁掉
}
}
private static String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
客户端程序
package TCP;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
//客户端通过Socket建立连接
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
//要想建立连接,就要知道服务器的地址
//由于TCP是有连接,所以要先连接上,所以new的时候要加上形参
socket = new Socket(serverIP,serverPort);
}
public void start(){
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();
//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();
}
}
上述服务器-客户端代码运行顺利
但是,一旦多开几个客户端代码,就会发现后面新的客户端不能使用,也就是说accept方法被第一个客户端占用着,后面的用不上
所以需要多线程
为什么之前写UDP代码的时候,多开几个客户端没事?
这是因为UDP直接发送消息即可.TCP建立连接之后,要处理客户端的多个请求,才导致无法快速调用accept
要是TCP每次只处理一个客户端的请求,也能保证快速调用accept
所以只要分多线程处理连接就行了
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//1.调用accept接收客户端的连接
//listenSocket就像是拉客的,拉完之后交给clientSocket来进行一对一处理
Socket clientSocket = listenSocket.accept();
//2.处理这个连接
Thread thread = new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
thread.start();
}
}
但是使用多线程会频繁的创建和销毁线程,这里就可以使用线程池
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//1.调用accept接收客户端的连接
//listenSocket就像是拉客的,拉完之后交给clientSocket来进行一对一处理
Socket clientSocket = listenSocket.accept();
//2.处理这个连接
ExecutorService service = Executors.newCachedThreadPool();//创建线程池
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
socket api是一切网络编程的基础,很多的框架/库/组件的底层都是基于socket, 所以这是很重要的!