目录
基于TCP的Socket通信
Tcp提供了基于流的长连接的数据传递,发送的数据带有顺序性。TCP是一种流协议,以六为单位进行数据传输。
什么是长连接?
长连接可以实现客户端与服务端连接成功后连续的传输数据,在这个过程中,连接保持开启状态。数据传输完后连接不关闭。
总之:长连接是指建立了Socket连接后,无论是否使用这个连接,该链接都保持连接状态。
什么是短连接?
短连接就是服务端与客户端连接成功后开始传输数据,数据传输完毕则连接立即关闭,如果还想传输数据,则需要再创建新的连接进行数据传输。
UDP是无连接协议,所以不存在长短连接的概念
长连接的优缺点
- 缺点:除第一次之外,客户端不需要每次传输数据时都先与服务端进行握手,这样就减少了握手确认时间,直接传输数据,提高程序运行效率
- 优点:在服务端需要保存多个Socket对象,增加内存占用
短连接的优缺点
- 缺点:每次传输数据前都要重新创建连接,也就是每次都要进行三次握手。
- 优点:在服务端不需要保存多个Socket对象,降低内存占用
1.1 ServerSocket类的accept()方法
ServerSocket的作用是创建Socket(套接字)的服务端,而Socket类的作用是创建Socket的客户端。在代码上就是使用Socket类去连接ServerSocket类,也就是客户端要主动连接服务端。
ServerSocket类中的 public Socket accept()方法的作用是真挺并接受此套接字的连接。此方法在连接传入之前一直阻塞。
ServerSocket accept()在没有客户端连接时是阻塞的
测试代码如下
如下代码创建了端口号为8088的服务端,然后等待客户端连接,没有客户端连接的话一直阻塞
@Test
public void test13() throws IOException {
System.out.println("服务器启动8088");
ServerSocket serverSocket = new ServerSocket(8088);
Socket accept = serverSocket.accept();
System.out.println("客户端连接成功 - ");
serverSocket.close();
}
创建客户端,先运行服务端,然后运行客户端,就会发现服务端不会阻塞了。
@Test
public void test14() throws IOException {
System.out.println("客户端启动");
//连接到本地的8088端口
Socket socket = new Socket("localhost",8088);
System.out.println("客户端结束 - > ");
socket.close();
}
使用ServerSocket创建一个Web服务器
@Test
public void test15(){
ServerSocket serverSocket = null;
Socket client = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
try {
serverSocket = new ServerSocket(8888);
client = serverSocket.accept();
//使用客户端写点数据出去
outputStream = client.getOutputStream();
inputStream = client.getInputStream();
//用转换流将其转换
InputStreamReader isr = new InputStreamReader(inputStream);
OutputStreamWriter osw = new OutputStreamWriter(outputStream);
//交给Buffer处理速度更快
bufferedReader = new BufferedReader(isr);
//将客户端的数据读取出来--打印--一行一行读取
String res = "";
while(!"".equals(res = bufferedReader.readLine())){
System.out.println(res);
}
//向客户端响应点数据进行测试
outputStream.write("HTTP/1.1 200 OK\r\n\r\n".getBytes());
outputStream.write("<html><body> <a href='www.baidu.com'>test</a> </body></html>".getBytes("UTF-8"));
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(outputStream != null){
outputStream.flush();
outputStream.close();
}
if(inputStream != null){
//外层被关闭,内层的InputStream也会关闭
bufferedReader.close();
}
client.close();
serverSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
1.2 Socket中的InputStream的read()方法
注意:此方法和accept()方法一样都具有阻塞特性
此方法的作用是从流中对方发来的数据。
测试程序
如下程序中启动服务器,然后启动客户端,会发现服务器一直阻塞,因为read()没读到任何数据,因为客户端没法送任何数据,当时在10s后就行了,因为客户端退出了
服务端如下所示
@Test
public void test16(){
try {
byte[] bytes = new byte[1024];
System.out.println("--------- 服务器启动 --------");
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
System.out.println("----------开始读取数据---------");
inputStream.read(bytes);
System.out.println("----------读取完成---------");
serverSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
客户端如下
@Test
public void test17(){
try {
System.out.println("客户端开始连接");
Socket socket = new Socket("127.0.0.1", 8888);
TimeUnit.SECONDS.sleep(10L);
System.out.println("客户端结束------");
socket.close();
}catch (IOException | InterruptedException e){
e.printStackTrace();
}
}
1.3 客户端向服务端传递字符串
代码分析
服务器端开设端口,等待客户端连接,
客户端连接成功后开始发送数据。
服务器端接受数据,但是每次只接受3个字符,并打印
服务器端如下所示
@Test
public void test18(){
ServerSocket serverSocket = null;
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
try {
char[] chars = new char[3];
serverSocket = new ServerSocket(8888);
Socket client = serverSocket.accept();
inputStream = client.getInputStream();
inputStreamReader = new InputStreamReader(inputStream);
int readLength = 0;
System.out.println("---------read start----------");
while((readLength = inputStreamReader.read(chars)) != -1){
String s = new String(chars, 0, readLength);
System.out.println(s);
}
System.out.println("---------read end----------");
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(inputStreamReader != null){
inputStreamReader.close();
}
if(serverSocket != null){
serverSocket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
客户端如下所示
@Test
public void test19(){
Socket socket = null;
OutputStream outputStream = null;
BufferedWriter bw = null;
try {
socket = new Socket("127.0.0.1",8888);
outputStream = socket.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(outputStream));
System.out.println("客户端写入数据开始");
TimeUnit.SECONDS.sleep(3L);
bw.write("我是东坡");
System.out.println("客户端写入结束");
}catch (IOException | InterruptedException e){
e.printStackTrace();
}finally {
try {
if(bw != null){
bw.flush();
bw.close();
}
if(socket != null){
socket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
1.4 服务端向客户端传递数据
服务器端如下
@Test
public void test20(){
ServerSocket serverSocket = null;
BufferedWriter bw = null;
try {
serverSocket = new ServerSocket(8888);
Socket client = serverSocket.accept();
bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
bw.write("我是东坡,这是测试");
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(bw != null){
bw.flush();
bw.close();
}
if(serverSocket != null){
serverSocket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
客户端如下
@Test
public void test21(){
Socket socket = null;
BufferedReader br = null;
try {
socket = new Socket("localhost",8888);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("客户端开始 -------");
String s = br.readLine();
System.out.println("客户端接收数据为 : "+ s);
System.out.println("客户端结束 -------");
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(br != null){
br.close();
}
if(socket != null){
socket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
1.5 实现服务端与客户端多次的往来通信
前面的实验都是服务端与客户端只进行了一次通信,那么如何实现多次的长连接通信呢?
代码分析
使用对象流ObjectOutputStream
服务端代码如下所示
@Test
public void test22(){
ServerSocket serverSocket = null;
ObjectInputStream ois = null;
ObjectOutputStream oos = null;
String msgA = "你好客户端A - 1\r\n";
String msgB = "你好客户端B - 1\r\n";
try {
serverSocket = new ServerSocket(8888);
Socket client = serverSocket.accept();
ois = new ObjectInputStream(client.getInputStream());
oos = new ObjectOutputStream(client.getOutputStream());
//先把长度发送过去,对面根据这个长度来创建数组
oos.writeInt((msgA + msgB).getBytes().length);
oos.write(msgA.getBytes());
oos.write(msgB.getBytes());
oos.flush();
//读取数据
int legth = ois.readInt();
byte[] bytes = new byte[legth];
ois.readFully(bytes);
System.out.println(new String(bytes));
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(oos != null){
oos.close();
}
if(ois!= null){
ois.close();
}
if(serverSocket != null){
serverSocket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
客户端代码如下所示
@Test
public void test23(){
Socket client = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
String msgA = "你好服务端A\r\n";
String msgB = "你好服务端B\r\n";
try {
client = new Socket("localhost",8888);
oos = new ObjectOutputStream(client.getOutputStream());
ois = new ObjectInputStream(client.getInputStream());
oos.writeInt((msgA + msgB).getBytes().length);
oos.write(msgA.getBytes());
oos.write(msgB.getBytes());
oos.flush();
int length = ois.readInt();
byte[] bytes = new byte[length];
ois.readFully(bytes);
System.out.println(new String(bytes));
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(oos != null){
oos.close();
}
if(ois!= null){
ois.close();
}
if(client != null){
client.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
1.6 Stream的close()方法造成Socket关闭
如下代码运行会出现SocketException socket is closed
原因如下所示:在关闭获取的流的时候,默认会关闭当前的socket,此时socket已经关闭,再次获取他的输出流就会抛出异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtESjquo-1603961482653)(images/异常.png)]
socket获取的输入流为SocketInputStream,其close方法如下
服务端如下所示
@Test
public void test1(){
try {
int len = 0;
byte[] bytes = new byte[20];
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务器启动");
Socket client = serverSocket.accept();
InputStream inputStream = client.getInputStream();
while((len = inputStream.read(bytes)) != -1){
System.out.println(new String(bytes,0,len));
}
inputStream.close();
OutputStream outputStream = client.getOutputStream();
client.close();
serverSocket.close();
System.out.println("服务器终止");
}catch (IOException e) {
e.printStackTrace();
}
}
客户端代码
@Test
public void test2(){
try {
Socket socket = new Socket("localhost", 8888);
System.out.println("客户端启动");
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是中国人".getBytes());
outputStream.close();
TimeUnit.SECONDS.sleep(100);
}catch (IOException | InterruptedException e){
e.printStackTrace();
}finally {
}
}
1.7 客户端传输图片到服务器
服务端代码
@Test
public void test3(){
ServerSocket serverSocket = null;
FileOutputStream fos = null;
try {
byte[] bytes = new byte[1024];
int len = 0;
serverSocket = new ServerSocket(8888);
System.out.println("服务器启动------");
Socket client = serverSocket.accept();
InputStream inputStream = client.getInputStream();
fos = new FileOutputStream("D:\\aa.jpg");
while((len = inputStream.read(bytes)) != -1){
fos.write(bytes,0,len);
}
fos.flush();
System.out.println("服务器保存文件数据成功------");
}catch (IOException e){
e.printStackTrace();
}finally {
//关闭流
try {
if(fos != null){
fos.close();
}
if(serverSocket != null){
serverSocket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
客户端代码
@Test
public void test4(){
Socket socket = null;
FileInputStream fis = null;
try {
byte[] bytes = new byte[1024];
int len = 0;
socket = new Socket("localhost", 8888);
System.out.println("连接服务器成功------");
fis = new FileInputStream("C:\\Users\\dongpo\\Pictures\\Saved Pictures\\1.jpg");
OutputStream outputStream = socket.getOutputStream();
while((len = fis.read(bytes)) != -1){
outputStream.write(bytes,0,len);
}
outputStream.flush();
System.out.println("客户端发送图片成功------");
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(fis != null){
fis.close();
}
if(socket != null){
socket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
1.8 TCP三次握手连接过程
什么是TCP的三次握手,如下图所示
各个参数的含义
- seq:发送数据的序列号
- ack:对于seq的确认,假设现在发送了100个字节的数据,如何确认?当ack=101时就代表前100个都收到了,因为101是他期待收到的数据。
- SYN:同步标志位:表示要请求连接
- ACK:确认标志位
三次握手的分析如下
第一次握手:
为什么seq=100?这里只是一个随机值,本来sql在设计的时候就是一个随机值。SYN=1?请求发起连接当然为1
第二次握手
为什么ack=101。服务现在想要接受第101个数据,说明我前面的都收到了。这是对客户端发送请求数据的一个确认,
ACK=1是确认标志位有效
第三次握手
为什么进行第三次握手:在第一次握手,服务器收到了这个连接请求并且给出确认。但是服务器并不知道我的确认客户端收没收到
如果只有两次握手:第二次的握手客户端没有收到,就会变成我不知道第一次发送的字节他收没收到,我下一次到底是从哪个序列开始发送数据:还是会从100开始发送数据
还有一种解释:当客户端发送第一个握手时,网络延时很长时间才到达了服务器端,而在中间时间超时了客户端的连接断开。之后服务器收到了客户端的请求误以为是请求连接的,然后并给出响应和确认,但是客户端已经断开了。如果只有两次握手此时服务器就会傻傻的等待客户端发送数据,殊不知客户端已经断开。
当有了三次握手之后:服务给到了客户端确认,当客户端收到了消息,再给服务器确认这样就避免了上面的问题。
1.9 服务端与客户端互传对象以及I/O流顺序问题
服务器代码
@Test
public void test7(){
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket client = serverSocket.accept();
System.out.println("客户端连接---"+client.getPort());
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
System.out.println("开始发送数据");
for(int i = 0; i<5;i++){
User o = (User)ois.readObject();
System.out.println("服务器 => "+i+"读取的对象"+o);
User newUser = new User(i, "server" + i);
oos.writeObject(newUser);
}
oos.close();
ois.close();
outputStream.close();
inputStream.close();
client.close();
serverSocket.close();
}catch (IOException | ClassNotFoundException e){
System.out.println("服务器异常");
}
}
客户端如下所示
@Test
public void test8(){
try {
Socket socket = new Socket("localhost", 8888);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
ObjectInputStream ois = new ObjectInputStream(inputStream);
for(int i = 0;i<5;i++){
oos.writeObject(new User(i,"client"));
//读取服务器发来的对象
User user = (User)ois.readObject();
System.out.println("client => "+i+user);
}
oos.close();
ois.close();
outputStream.close();
inputStream.close();
socket.close();
}catch (IOException | ClassNotFoundException e){
System.out.println("客户端异常");
}
}
此时如果将客户端获取输入流和输出流的代码调换位置,就会发生阻塞的情况
因为服务器端先获取输入流,在获取输出流
客户端也是先获取输入流在获取输出流,只要改变两个任意一个顺序就行