一、实验目的:
通过本实验,学习采用Socket(套接字)设计简单的网络数据收发程序,理解应用数据包是如何通过传输层进行传送的。
二、实验内容:
Socket(套接字)是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。一个socket允许应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。一台计算机上的应用程序向socket写入的信息能够被另一台计算机上的另一个应用程序读取,反之亦然。
不同类型的socket与不同类型的底层协议族以及同一协议族中的不同协议栈相关联。现在TCP/IP协议族中的主要socket类型为流套接字(socketssockets)和数据报套接字(datagram sockets)。流套接字将TCP作为其端对端协议(底层使用IP协议),提供了一个可信赖的字节流服务。一个TCP/IP流套接字代表了TCP连接的一端。数据报套接字使用UDP协议(底层同样使用IP协议),提供了一个"尽力而为"(best-effort)的数据报服务,应用程序可以通过它发送最长65500字节的个人信息。一个TCP/IP套接字由一个互联网地址,一个端对端协议(TCP或UDP协议)以及一个端口号唯一确定。
2.1、采用TCP进行数据发送的简单程序(java)
TCPClient.jave
package edu.net;
import java.io.*;
import java.net.*;
class TCPClient {
public static void main(String argv[]) throws Exception
{
String sentence;
String modifiedSentence;
BufferedReader inFromUser =
new BufferedReader(new InputStreamReader(System.in));
Socket clientSocket = new Socket("127.0.0.1", 6789);
DataOutputStream outToServer =
new DataOutputStream(clientSocket.getOutputStream());
BufferedReader inFromServer =
new BufferedReader(new
InputStreamReader(clientSocket.getInputStream()));
sentence = inFromUser.readLine();
outToServer.writeBytes(sentence + '\n');
modifiedSentence = inFromServer.readLine();
System.out.println("FROM SERVER: " + modifiedSentence);
clientSocket.close();
}
}
TCPServer.java
package edu.net;
import java.io.*;
import java.net.*;
class TCPServer {
private static ServerSocket welcomeSocket;
public static void main(String argv[]) throws Exception
{
String clientSentence;
String capitalizedSentence;
welcomeSocket = new ServerSocket(6789);
while(true) {
Socket connectionSocket = welcomeSocket.accept();
BufferedReader inFromClient =
new BufferedReader(new
InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient =
new DataOutputStream(connectionSocket.getOutputStream());
clientSentence = inFromClient.readLine();
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
}
}
}
程序功能说明:
先运行服务端,在运行客户端,在客户端窗口输出英文字母,服务器转化为大写字母后返回显示在客户端窗口上。
程序运行结果:
2.2、采用UDP进行数据发送的简单程序(java)
UDPClient.java
package edu.net;
import java.io.*;
import java.net.*;
class UDPClient {
public static void main(String args[]) throws Exception
{
BufferedReader inFromUser =
new BufferedReader(new InputStreamReader(System.in));
DatagramSocket clientSocket = new DatagramSocket();
InetAddress IPAddress = InetAddress.getByName("127.0.0.1");
byte[] sendData = new byte[1024];
byte[] receiveData = new byte[1024];
String sentence = inFromUser.readLine();
sendData = sentence.getBytes();
DatagramPacket sendPacket =
new DatagramPacket(sendData, sendData.length, IPAddress, 9876);
clientSocket.send(sendPacket);
DatagramPacket receivePacket =
new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);
String modifiedSentence =
new String(receivePacket.getData());
System.out.println("FROM SERVER:" + modifiedSentence);
clientSocket.close();
}
}
UDPServer.java
package edu.net;
import java.net.*;
class UDPServer {
private static DatagramSocket serverSocket;
public static void main(String args[]) throws Exception
{
serverSocket = new DatagramSocket(9876);
byte[] receiveData = new byte[1024];
byte[] sendData = new byte[1024];
while(true)
{
DatagramPacket receivePacket =
new DatagramPacket(receiveData, receiveData.length);
serverSocket.receive(receivePacket);
String sentence = new String(receivePacket.getData());
InetAddress IPAddress = receivePacket.getAddress();
int port = receivePacket.getPort();
String capitalizedSentence = sentence.toUpperCase();
sendData = capitalizedSentence.getBytes();
DatagramPacket sendPacket =
new DatagramPacket(sendData, sendData.length, IPAddress,
port);
serverSocket.send(sendPacket);
}
}
}
程序功能说明:
先运行服务端,在运行客户端,在客户端窗口输出英文字母,服务器转化为大写字母后返回显示在客户端窗口上。
程序运行结果:
2.3多线程\线程池对比
当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应。
并行服务器:可以单独处理没一个连接,且不会产生干扰。并行服务器分为两种:一客户一线程和线程池。
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(contextswitch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
我们可以通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
TCPEchoClient.java
package com.tcpip.examp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
/**
* TCP客户端
*
*
*/
public class TCPEchoClient
{
private static Socket socket;
public static void main(String[] args) throws UnknownHostException,
IOException
{
if (args.length < 2 || args.length > 3)
{
throw new IllegalArgumentException(
"Parameter(s): []");
}
// SerName or IP Address
String server = args[0];
// 0.1 获取传输文字的长度
byte[] data = args[1].getBytes();
// 0.2 获取端口号(严格的话要对这里进行一下端口的验证)
int serverPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
socket = new Socket(server, serverPort);
System.out.println("Connected to server ..... Sending echo String");
// 2.输入/输出
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// Sending encode string to server
out.write(data);
int totalBytesLength = 0;
int bytesRcvd;
while (totalBytesLength < data.length)
{
// 阻塞等待服务器端的返回
if ((bytesRcvd = in.read(data, totalBytesLength, data.length
- totalBytesLength)) == -1)
{
throw new SocketException("Connection closed prematurely");
}
totalBytesLength += bytesRcvd;
}
System.out.println("Received:"+new String(data));
// 3.关闭
socket.close();
}
}
TCPEchoServer.java
package com.tcpip.examp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
public class TCPEchoServer
{
private static final int BUFFER_SIZE = 128;
private static ServerSocket server;
public static void main(String[] args) throws IOException
{
if(args.length != 1)
{
throw new IllegalArgumentException("Parameter:");
}
// 获取服务器的端口
int serverPort = Integer.parseInt(args[0]);
server = new ServerSocket(serverPort);
System.out.println("Server has started!!!!");
int recvMsgSize = 0;
byte[] recvBuf = new byte[BUFFER_SIZE];
while(true)
{
System.out.println("Waiting from client connectting!!!");
// 阻塞等的客户端的连接
Socket socket = server.accept();
// 客户端的地址信息
SocketAddress address = socket.getRemoteSocketAddress();
System.out.println("Handling client at "+address+"\n");
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
while((recvMsgSize = in.read(recvBuf)) != -1)
{
out.write(recvBuf,0,recvMsgSize);
}
socket.close();
}
}
}
TCPEchoServerThread.java
package com.tcpip.examp;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;
/**
* 一客户一线程服务器处理
*
* 4.1 多任务处理
迭代服务器:当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应。
并行服务器:可以单独处理没一个连接,且不会产生干扰。并行服务器分为两种:一客户一线程和线程池。
4.1.1 Java多线程
Java提供了两种在一个新线程中执行任务的方法:1)为Thread类定义一个带有run()方法的子类,在run()方法中包含要执行的任务,并实例化这个子类;或2)定义一个实现了Runnable接口的类,并在run()方法中包含要执行的任务,再将这个类的一个实例传递给Thread的构造函数。
*
*/
public class TCPEchoServerThread
{
private static ServerSocket serverSocket;
public static void main(String[] args) throws IOException
{
if(args.length != 1)
{
throw new IllegalArgumentException("Parameter:");
}
// 日志记录器
Logger logger = Logger.getLogger("practical");
// 服务器侦听端口
int serverPort = Integer.parseInt(args[0]);
serverSocket = new ServerSocket(serverPort);
logger.info("Server is started!!!!");
while(true)
{
Socket socket = serverSocket.accept();
// 接收到客户端的请求后立即创建新的线程进行处理
Thread t = new Thread(new EchoProtocol(socket,logger));
t.start();
logger.info("Created and started Thread "+t.getName());
}
}
}
TCPEchoServerPool.java
package com.tcpip.examp;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;
/**
* 采用自定义线程池的方式实现多任务处理
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
我们可以通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
*
*/
public class TCPEchoServerPool
{
public static void main(String[] args) throws IOException
{
if(args.length != 2)
{
throw new IllegalArgumentException("Parameter: ");
}
// 服务器侦听的端口号
int serverPort = Integer.parseInt(args[0]);
// 自定义线程池的大小
int poolSize = Integer.parseInt(args[1]);
// 日志记录器
final Logger logger = Logger.getLogger("practical");
final ServerSocket serverSocket = new ServerSocket(serverPort);
logger.info("Server is started!!!!");
/*
* 每个线程都反复循环,从(共享的)ServerSocket实例接收客户端连接。
* 当多个线程同时调用同一个ServerSocket实例的accept()方法时,
* 它们都将阻塞等待,直到一个新的连接成功建立。然后系统选择一个线程,
* 新建立的连接对应的Socket实例则只在选中的线程中返回。
* 其他线程则继续阻塞,直到成功建立下一个连接和选中另一个幸运的线程。
*
* */
for(int i = 0; i < poolSize; i++)
{
Thread t = new Thread()
{
@Override
public void run()
{
while(true)
{
try
{
// 各个线程阻塞等待,知道有一个客户端连过来,当线程进行处理,其他的线程继续处理
Socket socket = serverSocket.accept();
EchoProtocol.handleEchoClient(socket, logger);
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
t.start();
logger.info("Thread "+t.getName()+" has started!!!");
}
}
}
EchoProtocol.java
package com.tcpip.examp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
public class EchoProtocol implements Runnable {
private static final int BUFSIZE = 32; // Size (in bytes) of I/O buffer
private Socket clientSocket; // Socket connect to client
private Logger logger; // Server logger
public EchoProtocol(Socket clientSocket, Logger logger) {
this.clientSocket = clientSocket;
this.logger = logger;
}
public static void handleEchoClient(Socket clientSocket, Logger logger) {
try {
// Get the input and output I/O streams from socket
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
int recvMsgSize; // Size of received message
int totalBytesEchoed = 0; // Bytes received from client
byte[] echoBuffer = new byte[BUFSIZE]; // Receive Buffer
// Receive until client closes connection, indicated by -1
while ((recvMsgSize = in.read(echoBuffer)) != -1) {
out.write(echoBuffer, 0, recvMsgSize);
totalBytesEchoed += recvMsgSize;
}
logger.info("Client " + clientSocket.getRemoteSocketAddress() + ", echoed " + totalBytesEchoed + " bytes.");
} catch (IOException ex) {
logger.log(Level.WARNING, "Exception in echo protocol", ex);
} finally {
try {
clientSocket.close();
} catch (IOException e) {
}
}
}
public void run() {
handleEchoClient(this.clientSocket, this.logger);
}
}
——当服务器选择TCPEchoServer时,有结果:
首先根据相应格式设置参数:
服务端:
客户端:
——当服务器选择TCPEchoServeThreadr时,有结果:
——当服务器选择TCPEchoServerPool时,有结果:
2.4写一个简单的chat程序,编程语言不限。
【源代码如下】
服务端Server:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
public class Server {
private List clients = null;
public static void main(String[] args) {
new Server().startUp();
}
private void startUp() {
ServerSocket ss = null;
Socket s = null;
try {
ss = new ServerSocket(5858);
clients = new ArrayList();
while (true) {
s = ss.accept();
ServerThread st = new ServerThread(s);
new Thread(st).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (ss != null)
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private class ServerThread implements Runnable {
private Socket s = null;
private BufferedReader br;
private PrintWriter out;
private String name;
private boolean flag = true;
public ServerThread(Socket socket) throws IOException {
this.s = socket;
//BufferedReader : 提供通用的缓冲方式文本读取,readLine读取一个文本行, 从字符输入流中读取文本,
//缓冲各个字符,从而提供字符、数组和行的高效读取。
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
String str = br.readLine();
//name = str+"["+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"]";
name = str;
clients.add(this);
send(name+"上线了");
}
private void send(String msg) {
for (ServerThread st : clients)
st.out.println(msg);
}
private void receive() throws IOException {
String str = null;
while ((str=br.readLine()) != null) {
if (str.equalsIgnoreCase("quit")) {
stop();
out.println("disconnect");
break;
}
send(name+":"+str);
}
}
private void stop() {
clients.remove(this);
flag = false;
send(name+"已经下线了");
}
@Override
public void run() {
try {
while (true) {
if (!flag) break;
receive();
}
} catch (SocketException e) {
stop();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (s != null)
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端1:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
private Socket s;
private BufferedReader br;
//PrintWriter向文本输出流打印对象的格式化表示形式。
private PrintWriter out;
private boolean flag = true;
public static void main(String[] args) {
new Client().stratUp();
}
private void stratUp() {
BufferedReader sbr = null;
try {
s = new Socket("127.0.0.1", 5858);
out = new PrintWriter(s.getOutputStream(), true);
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
out.println("端1");
// out.println("端2");
sbr = new BufferedReader(new InputStreamReader(System.in));
//InputStreamReader 是字节流通向字符流的桥梁,它将字节流转换为字符流.
//OutputStreamWriter是字符流通向字节流的桥梁,它将字符流转换为字节流.
new Thread(new ClientThread()).start();
String str = null;
while (flag && (str=sbr.readLine())!=null) {
if (!flag) break;
out.println(str);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (sbr != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void receive() {
try {
String rs = br.readLine();
if (rs.equalsIgnoreCase("disconnect")) {
flag = false;
System.out.println("点击回车退出");
}
System.out.println(rs);
} catch (IOException e) {
e.printStackTrace();
}
}
private class ClientThread implements Runnable {
@Override
public void run() {
while (true) {
if (!flag) break;
receive();
}
}
}
}
客户端2:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client2 {
private Socket s;
private BufferedReader br;
private PrintWriter out;
private boolean flag = true;
public static void main(String[] args) {
new Client2().stratUp();
}
private void stratUp() {
BufferedReader sbr = null;
try {
s = new Socket("127.0.0.2", 5858);
out = new PrintWriter(s.getOutputStream(), true);
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// out.println("端1");
out.println("端2");
sbr = new BufferedReader(new InputStreamReader(System.in));
new Thread(new ClientThread()).start();
String str = null;
while (flag && (str=sbr.readLine())!=null) {
if (!flag) break;
out.println(str);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (sbr != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void receive() {
try {
String rs = br.readLine();
if (rs.equalsIgnoreCase("disconnect")) {
flag = false;
System.out.println("点击回车退出");
}
System.out.println(rs);
} catch (IOException e) {
e.printStackTrace();
}
}
private class ClientThread implements Runnable {
@Override
public void run() {
while (true) {
if (!flag) break;
receive();
}
}
}
}
【运行结果】
1、 运行服务端server
2、 运行客户端client
3、 运行客户端client2
4、在端1窗口内输入消息:你好,端2!在端2窗口内输入消息:你好,端1!屏幕中可显示两个端口相互通信,即实现两个客户端通过服务器相互交流的功能!