计算机网络报告7

Exp7:基于TCP的Socket编程

⼀、实验⽬的

学习使⽤Stream Socket(包括ServerSocket和Socket)

了解粘包概念

⼆、实验任务

使⽤Socket和ServerSocket编写代码

解决粘包问题

三、实验过程

3.1 基础知识

3.1.1 Socket和ServerSocket交互过程

3.1.2 建⽴服务器端

  • 服务器建⽴通信ServerSocket
  • 服务器建⽴Socket接收客户端连接
  • 建⽴IO输⼊流读取客户端发送的数据
  • 建⽴IO输出流向客户端发送数据消息

3.1.3 建⽴客户端

创建Socket通信,设置通信服务器的IP和Port

建⽴IO输出流向服务器发送数据消息

建⽴IO输⼊流读取服务器发送来的数据消息

3.2 ⼩试⽜⼑: TCP传输案例

在src⽂件夹下新建⼀个Package,名为exp7

  • 编写TCPServer类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.company;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
public class TCPServer implements Runnable {
private static final int PORT = 9090;

@Override
public void run() {
ServerSocket server = null;
try {
server = new ServerSocket();
} catch (IOException e) {
e.printStackTrace();
}
// 配置⼀些参数
try {
server.setReuseAddress(true);
} catch (SocketException e) {
e.printStackTrace();
}
try {
server.setReceiveBufferSize(64 * 1024 * 1024);
} catch (SocketException e) {
e.printStackTrace();
}
// 绑定到本地端⼝上,backlog为50(请求在socket上的最⼤待处理连接数)
try {
server.bind(new InetSocketAddress(Inet4Address.getLocalHost(),
PORT), 50);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Server: 服务器已监听端⼝:");
System.out.println(server.getInetAddress() + ": " +
server.getLocalPort());
// 等待客户端连接
while (true) {
System.out.println("Server: 阻塞等待客户端连接中...");
Socket client = null;
try {
client = server.accept();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Server: 捕获成功");
// 在连接后,启动⼀个线程接管与客户端的交互操作
ClientHandler clientHandler = new ClientHandler(client);
clientHandler.start();
}
}

// 处理与客户端交互
private static class ClientHandler extends Thread {
private final Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
super.run();
System.out.println("Server: 新客户端连接:" + socket.getInetAddress() +
": " + socket.getPort());
try {
// 得到socket的输⼊输出流
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
byte[] bytes = new byte[1024];
// todo: task2 replace
int len = inputStream.read(bytes);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(new String(bytes, 0, len));
// todo: added for task1 to test multiple clients
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Server: 收到客户端消息:" + stringBuilder);
// 消息回写
outputStream.write(stringBuilder.toString().getBytes());
outputStream.close();
inputStream.close();
} catch (IOException e) {
System.out.println("Server: 连接异常断开");
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("Server: 客户端已退出:" + socket.getInetAddress() +
": " + socket.getPort());
}
}
}
  • 编写TCPClient类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.company;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
public class TCPClient implements Runnable {
private static final int PORT = 9090;


@Override
public void run(){
Socket socket = new Socket();
// 连接本地9090端⼝,timeout为3000ms
try {
socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(),
PORT), 3000);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Client: 客户端为:" + socket.getLocalAddress() + ": " +
socket.getLocalPort());
System.out.println("Client: 服务器为:" + socket.getInetAddress() + ": " +
socket.getPort());
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
OutputStream outputStream = null;
try {
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
String s = "test client send";
try {
outputStream.write(s.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes = new byte[1024];
// 接收服务器返回的消息
int len = 0;
try {
len = (inputStream.read(bytes));
} catch (IOException e) {
e.printStackTrace();
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(new String(bytes, 0, len));
System.out.println("Client: 收到服务器端消息:" + stringBuilder);
// 资源释放
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Client: 客户端已退出~");
}
}

task1:

分别运⾏Server端和2个Client端,请将运⾏结果附在实验报告中。

当我们不使用while(true)时,tcpserver只接受一个客户端传来的信息,如下图所示:

那么,当sever一直开启时:我们在main函数中创建多个client线程,向服务器发送连接请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.company;

import java.util.ArrayList;

public class Main {

public static void main(String[] args) {
// write your code here
Thread server = new Thread(new TCPServer());
ArrayList<Thread> clients = new ArrayList<Thread>();
for (int i = 0; i <6;i++){
Thread client = new Thread(new TCPClient());
clients.add(client);
}

server.start();
for (Thread client:clients)
client.start();
}
}

task2:

⽤下段代码修改TCPServer中// todo task2:replace 后的三⾏,试运⾏Server端和Client端,观察运⾏结果,并将实验结果及产⽣的原因附在实验报告中

1
2
3
4
5
int len;
StringBuilder stringBuilder = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
stringBuilder.append(new String(bytes, 0, len));
}

我们发现TCPServer和TCPClient已经建立了连接,但是在处理客户端信息时出现了错误,导致一直卡在哪儿不能继续运行下去。

1
2
3
4
int len = inputStream.read(bytes);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(new String(bytes, 0, len));
System.out.println("Server: 收到客户端消息:" + stringBuilder);

经过 测试,发现当 去掉While循环,便可以正常执行。

但这并不是一种好方法,因为当输入的 inputStream 信息很庞大,但是我只能读取前1024个字节。

其实,这里while循环不结束的原因是在client里面,当送出 outputStream之后,并没有关闭socket。因此server就会误以为后面还会有信息进来,因此一直在等待。虽然在client最后关闭了outputStream,但是那是在收到了server回传的信息之后了,这就形成了一个死锁。要解决这个问题,就要将这个socket关闭。 :

1
2
3
4
5
6
try {
outputStream.write(s.getBytes());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}

关闭后,恢复正常:

task3:

使⽤Scanner修改TCPClient类,达成如下效果,请将实验结果附在实验报告中:

1
2
3
4
client reads a line of characters from its keyboard and sends data to server
server receives the data and converts characters to uppercase
server sends modified data to client
client receives modified data and displays line on its screen

只需要修改client即可;

1
2
3
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
outputStream.write(s.getBytes());

task4:

在task3的基础上继续TCPServer类和TCPClient类,Client端能够读取多⾏从控制台输⼊的数据分别发送,Server端收到后分别回写(请测试数据为中⽂时的情况),请将实验结果附在实验报告中。

  • Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.company;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
public class Main {
private static final int PORT = 9090;
public static void main(String[] args) throws IOException {
.
System.out.println("服务器已监听端口:");
System.out.println(server.getInetAddress() + ": " +
server.getLocalPort());
// 等待客户端连接
while (true) {
System.out.println("阻塞等待客户端连接中...");
Socket client = server.accept();
System.out.println("连接成功");
// 在连接后,启动⼀个线程接管与客户端的交互操作
ClientHandler clientHandler = new ClientHandler(client);
clientHandler.start();
}
}
// 处理与客户端交互
private static class ClientHandler extends Thread {
private final Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
super.run();
System.out.println("新客户端连接:" + socket.getInetAddress() +
": " + socket.getPort());
try {
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
byte[] bytes = new byte[1024];
while(true){
int len = inputStream.read(bytes);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(new String(bytes, 0, len));
System.out.println("收到客户端消息:" + stringBuilder);
outputStream.write(stringBuilder.toString().getBytes());
}
} catch (IOException e) {
System.out.println("连接异常断开");
}
}
}
}
  • Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.company;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;

public class Main {
private static final int PORT = 9090;

public static void main(String[] args) throws IOException {
Socket socket = new Socket();
// 连接本地9090端⼝,timeout为3000ms
socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(),
PORT), 3000);
System.out.println("客户端为:" + socket.getLocalAddress() + ": " +
socket.getLocalPort());
System.out.println("服务器为:" + socket.getInetAddress() + ": " +
socket.getPort());
Scanner scanner = new Scanner(System.in);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
while (true) {
String s = scanner.nextLine();
outputStream.write(s.getBytes());
byte[] bytes = new byte[1024];
// 接收服务器返回的消息
int len = (inputStream.read(bytes));
System.out.println("收到服务器端消息:" + new String(bytes, 0, len));
}
}
}

3.3 什么是粘包

  • TCP本质上并不会发⽣数据层⾯的粘包
  • TCP的发送⽅和接收⽅⼀定会确保数据是以⼀种有序的⽅式到达的
  • 所谓的粘包是数据处理逻辑层⾯上的粘包,即应⽤层上的

task5:

查阅资料,分析粘包可能产⽣的原因并搜索若⼲种解决⽅法(>=3),并附在实验报告中。

在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。

粘包出现原因

  1. 发送端需要等缓冲区满才发送出去,造成粘包

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

  1. 接收方不及时接收缓冲区的包,造成多个包接收

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

为了避免粘包现象,可采取以下几种措施:

(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;但是但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。

(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;但是第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包

(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。但是第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。比如说认为确定一个消息边界。

-------------本文结束,感谢您的阅读-------------