文章目录
引言
- 上次面试腾讯的时候,反复提到了socket通信,不仅仅是不同机器不同进程之间的通信,同一个机器上不同进程之间的通信也是用这个,用的比较广泛,这里是我的知识盲区了,感觉面经上看到过好几次了,这里还是总结一下!补充一下
- 信息来源
正文
- Socket通信是进行端到端的通信,对于下层经过多少局域网、路由器是透明的。
- 进行Socket通信需要指定两个地方,分别是使用IP层协议和传输层协议!
- IP层协议
- IPv4==AF_INET
- IPv6==AF_INET6
- 传输层协议
- TCP》》基于字节流》》SOCK_STREAM
- UDP》》基于数据报》》SOCK_DGRAM
- IP层协议
基于TCP协议的Socket通信
-
在服务端进行Socket通信时,会维护两个Socket,一个是监听的Socket,还有一个是专门用来通信的已连接的Socket通信
- 监听Socket
- 服务器用来监听客户端链接请求的Socket,会一直等待连接请求,常规的监听8080端口
- 已连接Socket
- 当某一个客户端连接到服务器后,服务器会为客户端A生成一个新的Socket
- 监听Socket
-
此后,客户端A和服务器之间的消息传输都是通过这个已有连接的Socket进行,监听Socket继续等待其他客户端的连接请求
1、服务端设置监听Socket步骤
- bind
- 绑定IP地址和端口
- listen
- 服务端进入监听状态,客户端可以进行发起连接,当前的监听Socket会陷入阻塞
- 维系两个队列
- established状态的全连接队列
- syn_rcvd状态的半链接队列
- accept
- 返回一个新的已经建立的Socket,进行数据传输
2、服务器端创建通信Socket过程
- accept
- 创建一个新的socket,这个socket是表示服务器和某一个特定客户端的连接
- 当前链接的标识[客户端IP,客户端端口,监听Socket的IP,监听Socket的端口]
- send和revc
- 通过这三个数据进行手法数据
3、客户端发起连接创建Socket的过程
- connect
- 通过connect发起连接,指定目标IP地址和端口号
- 内核临时为客户端分配一个端口,进行三次握手
Socket的发送接收的底层实现
-
在Linux中是以文件的形式存在的,有对应的文件描述符,写入和读出都是通过文件描述符
- 每一个Socket中都有两个队列,分别是发送队列和接受队列
- 每一个队列保存完整的数据包
-
进程和文件的关系
- 每一个进程都有自己的task_struct,指向能够操作的文件描述符数组,每一个元素是指向内核中打开的文件的列表
- 进程通过操作对应文件结构,进行调用Socket进行发送数据和接受数据
基于UDP协议的Socket通信
- UDP通信比起TCP通信,有以下几个差异
- 没有链接
- 不需要三次握手,不需要在额外创建一个socket通信
- 不用调用listen和connect
- 直接bind即可,开启监听端口等
- 只需要使用sendto和recvfrom进行接收即可!
- 没有链接
服务端如何接收更多项目
- 最大连接数计算
- 标识连接的唯一标志——四元组
- {本机IP,本机端口,目标IP,目标端口}
- 2的32次方,2的16次方,最大连接数是2的48次方
- 受限因素
- 文件描述符限制,系统中ulimit限制
- 内存限制,每一个TCP链接都要占用内存,内存有限
多进程
-
实现
- 每次接收到一个新的socket,就会创建一个新的子进程解决
-
具体实现
- 父进程调用Fork创建子进程,执行如下操作
- 复制一个文件描述符列表
- 复制内存空间
- 指令计数器:当前程序运行的位置
- 父进程因为刚才accept创建的已经链接的Socket,也是一个文件描述符,同样会被子进程获得
- 子进程完成任务,通信完毕之后,退出返回0,标识自己是子进程
- 父进程调用Fork创建子进程,执行如下操作
多线程
-
实现
- 每次创建一个新的连接,创建一个新的socket,就创建一个新的线程解决
-
具体实现
- 主线程调用的pthread_create创建一个新的线程
- 新线程共享进程的资源
- 新线程通过通过已经连接的Socket请求,完成通信
- 主线程调用的pthread_create创建一个新的线程
没有办法解决C10K问题
IO多路复用==select轮询
-
实现
- 一个线程负责所有的Socket,依次查看每一个Socket,进行读写操作
-
具体实现
- Socket本身是文件描述符,保存在一个文件描述符集合中fd_set中
- 调用select函数,轮询所有的文件描述符是否有变化
- 将发生变化的文件描述符状态位设置为1
IO多路复用==epoll事件通知
- 同样都是多路复用,只不过这里的调用函数不一样,是使用epoll的方式,这里仅仅总结一下对应的差异就行了
- 注册通知替换轮询
- epoll_create创建监听的epoll对象
- 通过callback函数的方式,当某一个文件描述符发生变化的时候,主动通知
- 红黑树保存socket
- epoll_ctl实现节点加入红黑树
- 将需要监听的节点加入到红黑树中,快速定位发生时间的节点,然后进行监听。
- 注册通知替换轮询
使用Socket实现同一个机器上的多线程通信
服务端创建对应socket监听端口
- 使用java.net.Socket进行编程
- 创建对应的监听Socket
- 调用对应的accpet函数获取连接
- 每一个连接socket都交给一个新的线程处理
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
private static final int PORT = 12345;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("Server started on port " + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("New client connected: " + clientSocket.getInetAddress().getHostAddress());
// 每个连接交给一个新的线程处理
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
处理通信的线程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
) {
String message;
while ((message = in.readLine()) != null) {
System.out.println("Received from client: " + message);
out.println("Echo: " + message);
if (message.equalsIgnoreCase("exit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端发起对应的连接请求
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Main {
private static final String SERVER_ADDRESS = "localhost";
private static final int SERVER_PORT = 12345;
public static void main(String[] args) {
try (Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
System.out.println("Server response: " + in.readLine());
if (userInput.equalsIgnoreCase("exit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
面试题
TCP和UDP可以共用一个端口吗?
-
Socket五元组信息唯一确认的
- 协议类型
- 源IP地址
- 源端口
- 目标IP
- 目标端口
-
只要有一个信息不同,就是不同的socket,不会引起冲突!可以共用一个!
简单介绍一个socket
- 是应用层和传输层之间的接口,是一种编程接口的,用于网络通信
socket有哪两种类型?
-
在Socket编程中,常见的两种类型是TCP Socket和UDP Socket:
-
TCP Socket:基于TCP协议,提供面向连接的、可靠的、字节流的服务。在TCP协议中,建立连接通常需要进行三次握手,以保证数据的可靠传输。
-
UDP Socket:基于UDP协议,提供无连接的、不可靠的、数据报的服务。UDP协议不保证数据的可靠性,但传输速度较快,适用于实时性要求较高的场景。
Socket通信的原理
- 创建Socket:应用程序通过调用socket()函数创建一个Socket对象,并指定Socket的类型(如TCP或UDP)。
- 绑定地址和端口(仅服务器端):服务器端通过调用bind()函数将Socket对象与一个特定的IP地址和端口号绑定,这个端口就是服务器的标识,用于在网络上与其他主机建立连接。
- 监听连接(仅服务器端):服务器端通过调用listen()函数开始监听来自客户端的连接请求。
- 连接请求(客户端):客户端通过调用connect()函数向服务器发送连接请求,指定服务器的IP地址和端口号。
- 接受连接(服务器端):当服务器端监听到来自客户端的连接请求后,通过调用accept()函数接受连接请求,并返回一个新的Socket对象,用于与客户端进行通信。
- 数据传输:一旦建立连接,服务器和客户端就可以通过各自的Socket对象进行数据传输。它们通过读取和写入Socket对象上的数据流来发送和接收数据。
- 关闭连接:当通信完成或者出现错误时,可以通过关闭Socket对象来结束连接,释放资源。
总结
- 今天大概了解了Socket通信的相关内容,下次再遇到类型的题目,大差不差,都有一点印象,在考到,再拿过来复习就行了!
- 主要有以下几个部分
- Socket通信在TCP连接中的应用
- Socket通信在UDP连接中的应用
- 如何解决多个Socket通信的问题
- 本地如何使用Socket通信进行通信