通俗来说,Socket是套接字,是一种编程接口,类似于电话插口,通过Socket可以进行网络通信, 但是这种很不容易让人理解.
其实本质来说Socket就是四原组,包括 客户端ip: 客户端端口 + 服务端ip: 服务端端口, 其实就是一个对应关系, 通过这个四原组就可以唯一的确定一个数据包来自哪里,要发送到哪里.
从而可以使得不同服务器上的不同进程进行网络通信, 而数据不会乱掉, 这就是Socket.
Socket是TCP协议层的, 是内核级别的, 这个怎么理解?
在我们使用Socket编程时,服务需要执行accept()方法进行监听, 但是即使我们不执行这个方法, 也会进行三次握手,直接建立tcp连接, 内核会直接帮我们做这件事情, 这个后面有程序验证.
现在有一个客户端,IP地址用AIP代替, 还有一个服务端, IP地址是CIP
而通信其实是两个进程间的通信,我们知道服务端进程在启动的时候需要绑定监听一个端口, 比如是XPORT.
而客户端在发起建立连接的时候其实也会随机起一个端口, 比如是BPORT, 那双方建立连接的时候就会有一个四元组. 这个对应关系既在客户端存在,也在服务端存在
问题一: 服务端在建立连接后是否需要为客户端随机分配一个端口
不需要的, 因为当建立连接后, 不论是客户端还是服务端, 内核都会为这个连接分配资源, 在资源中都存储了这个对应关系(四元组), 所以不论是对于客户端还是服务端, 只要这个对应关系有, 这个对应关系是一个唯一标识, 就没有必要再分配一个端口号了.双方就可以进行通信.
问题二: 同一个客户端, 能不能启多个不同的进程去连接同一个服务端的某一个进程
是可以的, 比如客户端启了三个进程,占用的端口分别是BPORT,CPORT,DPORT, 那其实就会形成三个不同的四原组. 这三个都是唯一的, 自然不会影响.
问题三: 一个服务端的某个进程, 连接了很多不同的客户端, 那是怎么区分每个数据包是来自哪个客户端呢
其实在每个数据包中, 都会有这个对应关系, 也就是这个四元组, 这样就很容器区分了
问题四:默认linux机器能使用的端口是65535个, 那假如一个客户端起了很多的进程, 连接的都是同一个服务器的80端口, 把65535个端口都用了, 那现在这个客户端能不能再使用端口去连接相同服务器的其他端口 或者 不同服务器的其他端口
都是可以的, 因为客户端可以重复使用这些端口, 只要生成的四元组是唯一的, 比如以下, 某个客户端的B,C,D端口已经连接了某个服务端的X端口, 现在又用客户端的B,C,D端口连接了服务端的Y端口, 这六组对应关系都是唯一的, 就能够确定数据来源哪, 发送到哪, 那自然是没问题的
问题五: 那客户端可以使用相同的端口,但是服务端一个进程已经用了80端口, 再起一个程序去占用80端口就会报异常, 这是为什么呢
因为服务端是ServerSocket, ServerSocket还有些特殊, 需要先开启监听Listen, 开启监听后等待客户端的连接,
那假如服务端有两个程序A,B都使用了80端口开启监听等待连接, 此时有个客户端想要和A程序通信, 发起三次握手中第一次连接请求, 直接发给了80端口, 此时能分清是要和哪个程序建立连接吗? 此时是分不清的.
再举个生活中的通俗例子, 你有一个电话号码, 可以用这个电话号码打给市场监督管理局, 这个市场监督管理局的电话一定是唯一的, 不然你就会分不清, 但是你可以用同样的电话号码打给住建局, 那你其实就是客户端, 可以用相同的号码, 而公家单位市场监管局,住建键局这些,他们是服务端, 电话号码都是唯一的, 不能使用相同的.
接下来大致理一下不同机器上两个应用程序通过Socket通信流程
lsof -p 进程id --------- 查看某个进程的文件描述符,包括tcp这些
netstat -natp ------- 可以查看一个tcp连接建立的过程
tcpdump ------ 可以对tcp连接进行抓包
客户端 -> ServerClientTest
public class ServerClientTest {
public static void main(String[] args) {
try {
Socket client = new Socket("192.168.68.2", 9090);
client.setTcpNoDelay(true);
client.setSendBufferSize(20);
OutputStream outputStream = client.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while(true){
String line = reader.readLine();
if(line != null){
byte[] bytes = line.getBytes();
for (byte b : bytes) {
outputStream.write(b); //注意这里面没有调用flush
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端 -> ServerSocketTest
public class ServerSocketTest {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket();
server.bind(new InetSocketAddress(9090));
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("server up use 9090");
while(true){
try {
//阻塞,暂时不执行accept,分水岭
System.in.read();
//真正开启监听
Socket client = server.accept();
System.out.println("client port: "+client.getPort());
new Thread(() -> {
while(true){
try {
InputStream in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
char[] data = new char[1024];
int num = reader.read(data);
if(num>0){
System.out.println("client read some data :"+new String(data));
}else if(num==0){
System.out.println("client read data nothing......");
}else{
System.out.println("client read data error......");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
需要注意,在 服务端启动程序后,如果不进行任何输入, 程序会阻塞在这里, 不会调用accept()方法
对9090这个端口进行抓包
查看当前机器的所有的TCP连接,并没有9090端口的
启动服务端,但是还没有调用accept, 内核会主动注册一个LISTEN,绑定端口号
有了这个LISTEN之后, 客户端就可以连进来了
同时使用jps 命令查看刚刚启动的服务端的进程id是3907
使用lsof -p 3907
, 看看这个进程的文件描述符情况,可以看到有一个监听状态的文件描述符
注意此时,服务端还没有调用accept方法,启动服务端后没有做任何操作, 还阻塞在这行代码
使用tcpdump
查看抓包, 发现已经经过三次握手
使用netstat查看所有的TCP以及连接状态, 发现9090端口已经和一个客户端建立了连接, 但是这个Socket还没有分配给任何程序去使用
但是内核里面已经有它了
这里随便输入了一个回车, 跳过了 System.in.read(); 这行代码
程序里面真正有了一个tcp的文件描述符
内核中的socket也真正分配给了对应程序去使用
到这里, 你应该真正能明白Socket最主要就是四元组, 明白Socket是TCP协议层的, 是内核级别的.
今天的分享就到这里了,有问题可以在评论区留言,均会及时回复呀.
我是bling,未来不会太差,只要我们不要太懒就行, 咱们下期见.