网络编程套接字
是操作系统给应用程序提供的一组API(叫做socket API)
socket可以视为是应用层与传输层的通信桥梁
传输层的核心协议有两种 TCP UDP
socket API 也有对应的两组
由于TCP UDP协议差别很大,因此,这两组API差别也挺大
TCP与UDP的区别
TCP
(1)有连接
像打电话。得先接通,才能交互数据
(2)可靠传输
传输过程中,发送方知道接收方有没有收到数据
错误理解:
1.就是数据发过去之后100%能被对方收到
2. 可靠传输就是安全传输
(3)面向字节流
以字节为单位进行传输
(4)全双工
一条链路,双向通信
半双工:一条链路,单向通信
UDP
(1)无连接
像发微信。不用接通,直接就能发数据
(2)不可靠传输
传输过程中,发送方不知道接收方有没有收到数据
(3)面向数据流
以数据报为单位进行传输(一个数据报都会明确大小),一次发送 / 接受必须是一个完整的数据报,不能是半个,也不能是一个半。
(4)全双工
UDP socket
五元组:
一次通信是由五个核心信息描述出来的:源IP ,源端口, 目的IP ,目的端口,协议类型。
站在客户端的角度:
源IP:本机IP
源端口:系统分配的端口
目的IP:服务器的IP
目的端口:服务器端口
协议类型:UDP
站在服务器的角度:
源IP:服务器程序本机的IP
源端口:服务器绑定的端口(此处手动指定了9090)
目的IP:客户端IP(包含在收到的数据报中)
目的端口:客户端的端口(包含在收到的数据报中)
协议类型:UDP
UDP socket中,主要涉及到两个类
- DatagramSocket
创建了一个UDP版本的socket对象
Datagram:数据报
Socket对象,就代表操作系统中的一个socket文件。socket文件,代表网卡硬件设备抽象体现。
从socket文件读数据,本质就是读网卡
往socket文件写数据,本质就是写网卡 - DatagramPacket
代表了一个UDP数据报,使用UDP传输数据的基本单位.
每次发送/ 接收数据,都是在传输一个DatagramPacket对象
接下来写一个简单的客户端服务器程序,回显服务(EchoServer)
回显:请求的内容是什么,得到的回应就是什么
这样的程序属于最简单的网络编程中的程序,不涉及到任何的业务逻辑,就只是通过socket API单纯的转发
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("启动服务器");
//UDP不需要建立连接,直接接收从客户端发来的数据
while(true){
//1.读取客户端发来的请求
DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);//为了接收数据,需要先准备一个空的DatagarmPacket对象,由receive来进行填充数据
//把DatagramPacket解析成一个String
String request=new String(requestPacket.getData(),0, requestPacket.getLength(),"UTF-8");
//2.根据请求计算响应(这里是回显服务,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);
}
}
//由于是回显服务,响应就和请求一样了
//实际上对于一个真实的服务器来说,这个过程是非常复杂的
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server=new UdpEchoServer(9090);
server.start();
}
}
UDP客户端
public class UdpEchoClient {
private DatagramSocket socket=null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String ip,int port) throws SocketException {
//此处的port是服务器的端口
//客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的
socket=new DatagramSocket();
serverIP=ip;
serverPort=port;
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while(true){
//1.先从控制台读取用户输入的字符串
System.out.println("-> ");
String request=scanner.next();
//2.把这个用户输入的内容,构造成一个UDP请求,并发送
// 构造的请求里包含两部分信息
// 1)数据的内容,request字符串
// 2)数据要发给谁,服务器的 IP+端口
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.从服务器读取响应数据,并解析
DatagramPacket responsePacket=new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
//4.把响应结果显示到控制台上
System.out.printf("req:%s,resp:%s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
执行效果
翻译程序(英译汉)
接下来写一个简单的程序,带上业务逻辑。
请求是一些简单的英文单词,响应就是对应单词的翻译
客户端不变,把服务器的代码进行调整,主要是process方法。
public class UdpDictServer extends UdpEchoServer{
private HashMap<String,String> dict=new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("monkey","小猴");
dict.put("cow","小牛");
dict.put("wolf","小狼");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"该次无法解释");
}
public static void main(String[] args) throws IOException {
UdpDictServer server=new UdpDictServer(9090);
server.start();
}
}
执行效果
TCP流套接字编程
TCP api中,也是涉及两个核心的类
ServerSocket(专门给TCP服务器用的)
Socker(既需要给服务器用,又需要给客户端用)
TCP服务器
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){
//由于TCP是有连接的,不能一上来就读数据,要先建立连接(接电话)
//accept就是在接电话,接电话的前提就是有人给打电话,如果当前没有客户端建立连接,此处的accept就会阻塞
//accept返回了一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就只干了一件事,就是建立连接
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和响应
//这里的针对TCP socket读写 就和文件读写是一模一样的
try(InputStream inputStream= clientSocket.getInputStream()) {
try(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.把响应返回给客户端
//为了方便起见,可以使用PrintWriter把OutputStream包裹
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果没有这个刷新,可能客户端就看不到响应结果
printWriter.flush();
System.out.printf("[%s:%d] req:%s,resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//此处要记得关闭
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(9090);
server.start();
}
}
TCP客户端
public class TcpEchoClient {
//用普通的socket即可
//此处也不用手动给客户端指定端口号,让系统自由分配
private Socket socket=null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//这里的IP和端口号的含义表示的不是自己绑定,而是表示和这个IP 端口号建立连接
//调用这个构造方法,就会和服务器建立连接(打电话拨号了)
socket=new Socket(serverIP,serverPort);
}
public void start(){
System.out.println("和服务器连接成功");
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream= socket.getInputStream()){
try(OutputStream outputStream= socket.getOutputStream()){
while (true) {
//1.从控制台读取字符串
System.out.print("->");
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.printf("req:%s,resp:%s\n",request,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();
}
}
运行结果
虽然上述的TCP代码已经跑起来了,但是此处还存在一个很严重的问题,当前的服务器,同一时刻只能处理一个连接
要想解决上述问题,就得让processConnection和前面的accept的执行互相不干扰,不能让processConnection里面的循环导致accept无法及时调用。
此处的解决方案是使用多线程。
为什么UDP版本的程序没用多线程,也是可以的?
UDP不需要处理连接,UDP只需要一个循环 ,就可以处理所有客户端的额请求。但是,TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会影响到外层循环的进度了。
主线程,循环调用accept,当有客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干个请求提供服务(在新线程里,通过while循环来处理请求),这个时候多个线程是并发执行的关系(宏观上看起来同时执行)就是各自执行各自的了,就不会互相干扰了。(每个客户端连接上来的时候,都要分配一个线程)
改进之后:
1.只改外层循环
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//由于TCP是有连接的,不能一上来就读数据,要先建立连接(接电话)
//accept就是在接电话,接电话的前提就是有人给打电话,如果当前没有客户端建立连接,此处的accept就会阻塞
//accept返回了一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就只干了一件事,就是建立连接
Socket clientSocket=serverSocket.accept();
//【改进方法】在这个地方,每次accept成功,都创建一个新的线程,由新线程负责执行合格processConnection方法
Thread t=new Thread(()->{
processConnection(clientSocket);
});
t.start();
}
}
2.使用线程池
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService pool= Executors.newCachedThreadPool();
while(true){
//由于TCP是有连接的,不能一上来就读数据,要先建立连接(接电话)
//accept就是在接电话,接电话的前提就是有人给打电话,如果当前没有客户端建立连接,此处的accept就会阻塞
//accept返回了一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
//进一步讲,serverSocket就只干了一件事,就是建立连接
Socket clientSocket=serverSocket.accept();
//【改进方法】使用线程池
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
翻译程序(英译汉)
public class TcpDictServer extends TcpThreadPoolEchoServer{
private HashMap<String,String> dict=new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("monkey","小猴");
dict.put("pig","小猪");
}
@Override
public String process (String request){
return dict.getOrDefault(request,"当前的词无法解释");
}
public static void main(String[] args) throws IOException {
TcpDictServer server=new TcpDictServer(9090);
server.start();
}
}
执行效果
一个TCP服务器,能否让一个UDP客户端连上?
不能。TCP和UDP无论是API代码,还是协议底层的工作过程,都是差异巨大的。
一次通信,需要用到五元组,协议类型不匹配,通信是无法完成的。