一序
作者介绍了客户端-服务器范式,及套接字socket的接口及常用函数,后面分别列举了UDP和TCP的demo。
这章大概了解下,demo又是C的例子,之前的文档介绍了UDP demo。
所以这里举个TCP的demo。
二 socket关键字
Socket通信步骤:(简单分为4步)
1.建立服务端ServerSocket和客户端Socket
2.打开连接到Socket的输出输入流
3.按照协议进行读写操作
4.关闭相对应的资源
相关API类 ServerSocket
此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
服务器套接字的实际工作由 SocketImpl 类的实例执行。应用程序可以更改创建套接字实现的套接字工厂来配置它自身,从而创建适合本地防火墙的套接字。
一些重要的方法:(具体大家查看官方api吧)
ServerSocket(int port, int backlog)
利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。
bind(SocketAddress endpoint, int backlog)
将 ServerSocket 绑定到特定地址(IP 地址和端口号)。
accept()
侦听并接受到此套接字的连接
getInetAddress()
返回此服务器套接字的本地地址。
close()
关闭此套接字。
类 Socket
此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
套接字的实际工作由 SocketImpl 类的实例执行。应用程序通过更改创建套接字实现的套接字工厂可以配置它自身,以创建适合本地防火墙的套接字。
一些重要的方法:(具体大家查看官方api吧)
Socket(InetAddress address, int port)
创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
getInetAddress()
返回套接字连接的地址。
shutdownInput()
此套接字的输入流置于“流的末尾”。
shutdownOutput()
禁用此套接字的输出流。
close()
关闭此套接字。
demo
server
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
try {
//1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket=new ServerSocket(8888);
Socket socket=null;
//记录客户端的数量
int count=0;
System.out.println("***服务器即将启动,等待客户端的连接***");
//循环监听等待客户端的连接
while(true){
//调用accept()方法开始监听,等待客户端的连接
socket=serverSocket.accept();
//创建一个新的线程
ServerThread serverThread=new ServerThread(socket);
//启动线程
serverThread.start();
count++;//统计客户端的数量
System.out.println("客户端的数量:"+count);
InetAddress address=socket.getInetAddress();
System.out.println("当前客户端的IP:"+address.getHostAddress());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
thread
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerThread extends Thread {
// 和本线程相关的Socket
Socket socket = null;
public ServerThread(Socket socket) {
this.socket = socket;
}
//线程执行的操作,响应客户端的请求
public void run(){
InputStream is=null;
InputStreamReader isr=null;
BufferedReader br=null;
OutputStream os=null;
PrintWriter pw=null;
try {
//获取输入流,并读取客户端信息
is = socket.getInputStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
String temp = null;
String info = "";
while ((temp = br.readLine()) != null) {
info += temp;
System.out.println("已接收到客户端连接");
System.out.println("服务端接收到客户端信息:" + info + ",当前客户端ip为:"
+ socket.getInetAddress().getHostAddress());
}
socket.shutdownInput();//关闭输入流
//获取输出流,响应客户端的请求
os = socket.getOutputStream();
pw = new PrintWriter(os);
pw.write("hello "+info);
pw.flush();//调用flush()方法将缓冲输出
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//关闭资源
try {
if(pw!=null)
pw.close();
if(os!=null)
os.close();
if(br!=null)
br.close();
if(isr!=null)
isr.close();
if(is!=null)
is.close();
if(socket!=null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
client
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class TCPClient {
public static void main(String[] args) {
try {
//1.创建客户端Socket,指定服务器地址和端口
Socket socket=new Socket("127.0.0.1", 8888);
//2.获取输出流,向服务器端发送信息
OutputStream outputStream=socket.getOutputStream();//获取一个输出流,向服务端发送信息
PrintWriter printWriter=new PrintWriter(outputStream);//将输出流包装成打印流
User user=new User("admin","123456");
printWriter.print(user.toString());
printWriter.flush();
socket.shutdownOutput();//关闭输出流
//3.获取输入流,并读取服务器端的响应信息
InputStream is=socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String info=null;
while((info=br.readLine())!=null){
System.out.println("我是客户端,服务器说:"+info);
}
//4.关闭资源
br.close();
is.close();
printWriter.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
还有个user就不贴了。
服务端输出:
客户端输出:
我是客户端,服务器说:hello User [name=admin, password=123456]
抓包
红框内就是一个交互过程。
这个流程我们看121-123就是3次握手建立连接。
124是psh客户端给服务器发送数据
126是server返回的ack。
125 开始就是进入4次挥手断开链接。125客户端发送FIN。seq=35
127 是psh跟ack.其中psh是server 王客户端推送数据,ack是确认125,ack=36
128是server发送FIN,seq=41,ack=36
129是客户端ack。seq=36,ack=42
可以结合图来理解
再看下抓包的头部数据
前面是帧头,ip头,前面的UDP有介绍,就不展开了。后面包含了tcp头跟data。
因为tcp头固定长度20字节,可变长度40字节,主要是在链接建立阶段。所以再看看tcp头部数据
源端口号:52173,对应cbcd
目的端口号:8888,对应22 b8
序列号seq:传输数据过程中,为每一个封包分配一个序号,保证网络传输数据的顺序性 ad 3a 01 26
确认号ack:用来确认确实有收到相关封包,内容表示期望收到下一个报文的序列号,用来解决丢包的问题 00 00 00 00
首部长度:8
标志位:这部分主要标志数据包的属性,其中syn是请求同步。02
窗口大小:作用:拥塞流控:8192对应2000
校验和:3f70
紧急指针:可以告知紧急的数据位置,0
下面是选项:
最大报文传输段(Maximum Segment Size ---MSS)
1460
MSS 是TCP选项中最经常出现,也是最早出现的选项。MSS选项占4byte。MSS是每一个TCP报文段中数据字段的最大长度,注意:只是数据部分的字段,不包括TCP的头部。TCP在三次握手中,每一方都会通告其期望收到的MSS(MSS只出现在SYN数据包中)如果一方不接受另一方的MSS值则定位默认值536byte。 MSS值太小或太大都是不合适。太小,例如MSS值只有1byte,那么为了传输这1byte数据,至少要消耗20字节IP头部+20字节TCP头部=40byte,这还不包括其二层头部所需要的开销,显然这种数据传输效率是很低的。MSS过大,导致数据包可以封装很大,那么在IP传输中分片的可能性就会增大,接受方在处理分片包所消耗的资源和处理时间都会增大,如果分片在传输中还发生了重传,那么其网络开销也会增大。因此合理的MSS是至关重要的。MSS的合理值应为保证数据包不分片的最大值。对于以太网MSS可以达到1460byte。
IP有个类似的概念MTU
MTU=MSS+TCP Header+IP Header.
窗口大小
Windows scaling 占3个byte,其中的一个字节表示移位值S。新的窗口值等于TCP首部的窗口位数从16增大到(16+S)。这相当于把窗口值向左移动S位后获得实际的窗口大小。移位值准许使用的最大值是14,相当于窗口最大值增大到65535*2^14 也就是 1GB
这里是2,意思是扩大4倍。
SACK: permited
选择确认。
NOP NOP(no operation) 。设计该字段主要是用来提供填充垫片。TCP的头部必须是4byte的倍数,但是大多数的TCP选项不是4byte的倍数。假如出现了整个TCP选项部分不是4byte的倍数,那么就需要使用1或多个字节无意义的nop 来填充,使之符合TCP的头部构造的规定.以及选项间填充。