本章在TCP/IP的基础上介绍如果使用Java语言来实现Socket通信,如何使用ServerSocket类来处理服务端server,如果使用Socket类处理客户端。
基于UDP时,会使用DatagramSocket类处理服务端与客户端之间的Socket通信,传输的数据要放在DatagramPacket类中。
4.1 基于TCP的Socket通信
TCP是基于“流”的“长连接”的数据传递,发送的数据带有顺序性。TCP是一种流协议,以流为单位进行传输。
什么是长连接?长连接可以实现服务端与客户端连接成功后连续地传输数据,在这个过程中,连接保持开启的状态,数据传输完毕后连接不关闭。长连接是指建立Socket连接后,无论是否使用这个连接,该连接都保持连接的状态。
什么是短连接? 短连接是当服务器与客户端连接成功后开始传输数据,数据传输完毕后则立刻关闭,如果还想再次传输数据,则需要在创建新的连接进行数据传输。
4.1.1 验证ServerSocket类的accept()方法具有阻塞特性
ServerSocket类的作用是创建Socket(套接字)的服务端,而Socket类的作用是创建Socket的客户端。
在代码层面使用的方式就是Socket类去连接ServerSocket类,也就是客户端要主动连接服务端。
ServerSocket类中的public Socket accept()方法的作用是侦听并接受此套接字的连接。此方法在连接传入之前一直阻塞。
先运行Server类,在运行Client类
public class Server {
public static void main(String[] args) {
try {
//8088是服务器的端口号
ServerSocket socket = new ServerSocket(8088);
System.out.println("server服务器开始运行"+System.currentTimeMillis());
socket.accept();
System.out.println("server服务器运行结束"+System.currentTimeMillis());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
--------------------
public class Client {
public static void main(String[] args) {
System.out.println("客户端准备连接"+System.currentTimeMillis());
try {
//localhost为服务器地址,8088为服务器端口号
Socket socket = new Socket("localhost",8088);
System.out.println("客户端连接结束"+System.currentTimeMillis());
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果分别为:
server服务器开始运行1558706485129
server服务器运行结束1558706488488
客户端准备连接1558706488451
客户端连接结束1558706488488
public Socket(String host,int port)的第一个参数host可以写成IP地址或域名。
4.1.2 使用ServerSocket类创建一个Web服务器
public class WebServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(6666);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String getString = "";
while(!"".equals(getString = bufferedReader.readLine())) {
System.out.println(getString);
}
OutputStream outputStream = socket.getOutputStream();
outputStream.write("HTTP/1.1 200 OK\r\n\r\n".getBytes());
outputStream.write("baidu good!".getBytes());
outputStream.flush();
inputStream.close();
outputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:控制台
GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (MSIE 9.0; qdesk 2.4.1279.203; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko Core/1.70.3676.400 QQBrowser/10.4.3469.400
Host: localhost:6666
Connection: Keep-Alive
4.1.3 验证Socket中InputStream类的read类的read方法也具有阻塞特性
public class Server2 {
public static void main(String[] args) {
byte[] byteArray = new byte[1024];
try {
ServerSocket serverSocket = new ServerSocket(8088);
System.out.println("accept begin"+System.currentTimeMillis());
Socket socket = serverSocket.accept();
System.out.println("accept end"+System.currentTimeMillis());
InputStream inputStream = socket.getInputStream();
System.out.println("read begin "+System.currentTimeMillis());
int readLength = inputStream.read(byteArray);
System.out.println("read end"+System.currentTimeMillis());
inputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
---------------
public class Client2 {
public static void main(String[] args) throws InterruptedException {
System.out.println("socket begin"+System.currentTimeMillis());
try {
Socket socket = new Socket("localhost",8088);
System.out.println("socket end"+System.currentTimeMillis());
Thread.sleep(Integer.MAX_VALUE);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端运行结果:
accept begin1558708736890
accept end1558708748974
read begin 1558708748975
服务器端被阻塞在read()方法,因为客户端没有发送数据。
4.1.4 客户端向服务器端传递字符串
public class Server3 {
public static void main(String[] args) {
char[] charArray = new char[5];
try {
ServerSocket serverSocket = new ServerSocket(8808);
System.out.println("accept begin"+System.currentTimeMillis());
Socket socket = serverSocket.accept();
System.out.println("accept end"+System.currentTimeMillis());
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
System.out.println("read begin"+System.currentTimeMillis());
int readLength = inputStreamReader.read(charArray);
while(readLength!=-1) {
String newString = new String(charArray,0,readLength);
System.out.println(newString);
readLength = inputStreamReader.read(charArray);
}
System.out.println("read end"+System.currentTimeMillis());
inputStreamReader.close();
inputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
--------------------------------
public class Client3 {
public static void main(String[] args) {
System.out.println("socket begin"+System.currentTimeMillis());
try {
Socket socket = new Socket("localhost",8808);
System.out.println("socket end"+System.currentTimeMillis());
Thread.sleep(3000);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是外星人".getBytes());
outputStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:服务器端
accept begin1558709644382
accept end1558709667305
read begin1558709667306
我是外星人
read end1558709670305
4.1.5 服务器端向客户端传递数据
public class Server4 {
public static void main(String[] args) {
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(8808);
System.out.println("accept begin"+System.currentTimeMillis());
Socket socket = serverSocket.accept();
System.out.println("accept end"+System.currentTimeMillis());
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是服务器".getBytes());
outputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
------------------------
public class Client4 {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8808);
char[] charArray = new char[3];
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
System.out.println("read begin"+System.currentTimeMillis());
int readLength = inputStreamReader.read(charArray);
while(readLength!=-1) {
String newString = new String(charArray,0,readLength);
System.out.println(newString);
readLength = inputStreamReader.read(charArray);
}
System.out.println("read end"+System.currentTimeMillis());
inputStreamReader.close();
inputStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
read begin1558710029132
我是服
务器
read end1558710029132
4.1.6 运行多次调用write()方法进行写入操作
write()方法允许多次被调用,每执行一次就代表传递一次数据。
4.1.7 使用Socket传递PNG图片文件
public class Server5 {
public static void main(String[] args) {
byte[] byteArray = new byte[2048];
try {
ServerSocket serverSocket = new ServerSocket(8088);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
int readLength = inputStream.read(byteArray);
FileOutputStream stream = new FileOutputStream("picture/new.jpg");
while(readLength!=-1) {
stream.write(byteArray,0,readLength);
readLength = inputStream.read(byteArray);
}
stream.close();
inputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
-----------------------------
public class Client5 {
public static void main(String[] args) {
String source = "picture/1.jpg";
try {
FileInputStream stream = new FileInputStream(new File(source));
byte[] byteArray = new byte[2048];
Socket socket = new Socket("localhost",8088);
OutputStream output = socket.getOutputStream();
int readLength = stream.read(byteArray);
while(readLength!=-1) {
output.write(byteArray,0,readLength);
readLength = stream.read(byteArray);
}
output.close();
stream.close();
socket.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.1.8 结合多线程Thread实现通信
public class ReadRunnable implements Runnable{
private Socket socket;
public ReadRunnable(Socket socket) {
super();
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
byte [] byteArray = new byte[100];
int readLength = inputStream.read(byteArray);
while(readLength !=-1) {
System.out.println(new String(byteArray,0,readLength));
readLength = inputStream.read(byteArray);
}
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
--------------------------------------------
public class Server {
private ServerSocket serverSocket;
private Executor pool;
public Server(int port,int poolSize) {
try {
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
} catch (IOException e) {
e.printStackTrace();
}
}
public void StartService() {
for(;;) {
try {
Socket socket = serverSocket.accept();
pool.execute(new ReadRunnable(socket));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Server server = new Server(8088,10000);
server.StartService();
}
}
4.1.9 结合多线程Thread实现通信
public class Server {
private ServerSocket serverSocket;
private Executor pool;
public Server(int port,int poolSize) {
try {
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
} catch (IOException e) {
e.printStackTrace();
}
}
public void StartService() {
for(;;) {
try {
Socket socket = serverSocket.accept();
pool.execute(new ReadRunnable(socket));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Server server = new Server(8088,10000);
server.StartService();
}
}
4.1.10 服务端与客户端互传对象以及I/0流顺序问题
public class Userinfo implements Serializable{
private long id;
private String username;
private String password;
public Userinfo() {
}
public Userinfo(long id, String username, String password) {
super();
this.id = id;
this.username = username;
this.password = password;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
------------------------
public class Server {
public static void main(String[] args) throws ClassNotFoundException {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
for(int i = 0;i<5;i++) {
Userinfo userinfo = (Userinfo)objectInputStream.readObject();
System.out.println("在服务端打印"+(i+1)+":"+userinfo.getId()+""+userinfo.getUsername()+""+userinfo.getPassword());
Userinfo newUserinfo = new Userinfo();
newUserinfo.setId(i+1);
newUserinfo.setUsername("serverUsername"+(i+1));
newUserinfo.setPassword("serverPassword"+(i+1));
objectOutputStream.writeObject(newUserinfo);
}
objectOutputStream.close();
objectInputStream.close();
outputStream.close();
inputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
-------------------------------------
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Socket socket = new Socket("localhost",8888);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
for(int i = 0;i<5;i++) {
Userinfo newUserinfo = (Userinfo)objectInputStream.readObject();
newUserinfo.setId(i+1);
newUserinfo.setUsername("clientUsername"+(i+1));
newUserinfo.setPassword("clientPassword"+(i+1));
objectOutputStream.writeObject(newUserinfo);
Userinfo userinfo = (Userinfo)objectInputStream.readObject();
System.out.println("在服务端打印"+(i+1)+":"+userinfo.getId()+""+userinfo.getUsername()+""+userinfo.getPassword());
}
objectOutputStream.close();
objectInputStream.close();
outputStream.close();
inputStream.close();
socket.close();
}
}
4.2 ServerSocket类的使用
4.2.1 接受accept与超时Timeout
public Socket accept()方法的作用就是侦听并接受此套接字的连接。此方法在连接传入之前一直阻塞。
setSoTimeout(timeout)方法的作用是设置超时时间,通过指定超时timeout值启用/禁用SO_TIMEOUT,以ms为单位。
4.2.2 构造方法的backlog参数的意义
public ServerSocket(int port,int backlog)中的参数backlog的主要作用就是允许接受客户端连接请求的个数。
传入backlog参数的作用是设置最大等待队列长度,如果队列已满,则拒绝该连接。默认值是50.
4.2.3 构造方法ServerSocket(int port,int backlog,InetAddress bindAddr)的使用
public ServerSocket(int port,int backlog,InetAddress bindAddr)的作用是使用指定的port和backlog将Socket绑定到本地InetAddress bindAddr来创建服务器。bindAddr参数可以在ServerSocket的多宿主主机上使用,ServerSocket仅接受对其多个地址的其中一个的连接请求。如果bindAddr为null,则默认接受任何/所有本地地址上的连接。
多宿主主机代表1台计算机有两块网卡,每个网卡有不同的IP地址,也有可能出现1台计算机有1块网卡,但这块网卡有多个IP地址的情况。
4.2.4 绑定到指定的Socket地址
public void bind(SocketAddress endpoint)方法的主要作用是将ServerSocket绑定到特定的Socket地址,使用这个地址与客户端进行通信。
SocketAddress是个抽象类。子类是InetSocketAddress,代表Socket地址
InetAddress类代表IP地址。
4.2.5 InetSocketAddress类介绍
InetSocketAddress类有3个构造方法:
public InetSocketAddress(int port)的作用是创建套接字地址,其中IP地址为通配符地址,端口号为指定值。
public InetSocketAddress(String hostname,int port)的作用是根据主机名和端口号创建套接字地址。
public InetSocketAddress(InetAddress addr,int port)的作用根据IP地址和端口号创建套接字地址。
4.2.6 获取本地SocketAdress对象以及本地端口
getLocalSocketAddress()方法用来获取本地的SocketAddress对象,它返回此Socket绑定的端点的地址。
4.2.7 判断Socket绑定状态
public boolean isBound()方法的作用是返回ServerSocket的绑定状态。如果将ServerSocket成功绑定到一个一个地址,则返回true.
4.2.8 Socket选项ReuseAddress
public void setReuseAddress(boolean on)方法的作用是启用/禁用SO_REUSEADDR套接字选项。
如果在使用bind(SocketAddress)方法“绑定套接字之前”启用SO_REUSERADDR选项,就可以允许绑定到处于超时状态的套接字。
public boolean getReuseAddress()的方法的作用是测试是否启用SO_REUSERADDR
4.2.9 Socket选项ReceiveBufferSize
public void setReceiveBufferSize(int size)方法的作用是为从此ServerSocket接受的套接字的SO_REVEUF选项重新设置新的建议值。在接受的套接字中,实际被采纳的值必须在accept()方法返回套接字后通过调用Socket.getReceiveBufferSize()方法进行获取。
SO_RCVBUF的值用于设置内部套接字接受缓冲区的大小和设置公布到远程同位体的TCP接收窗口的大小。随后可以通过调用Socket.setReceiveBufferSize(int)方法更改该值。但是,如果应用程序希望允许大于RFC1323中定义的64KB的接收窗口,则在将ServerSocket绑定到本地地址之前必须在其中设置建议值。
这意味着,必须用无参构造方法创建ServerSocket,然后必须调用setReceiveBufferSize()方法,最后通过调用bind()将ServerSocket绑定到地址。
public int getReceiveBufferSize()方法的作用是获取此ServerSocket的SO_RCVBUF选项的值,该值是将用于从此ServerSocket接受的套接字的建议缓冲区大小。
注意,对于客户端,SO_RCVBUF选项必须在connect方法调用之前设置,对于服务端,SO_RCVBUF选项必须在bind()前设置。
4.3 Socket类的使用
ServerSocket类的作用是搭建Socket的服务端环境,而Socket类的主要作用是使Server与Client进行通信。
4.3.1 绑定bind与connect以及端口生成的时机
public void bind(SocketAddress bindpoint)方法的作用是将套接字绑定到本地地址。
4.3.2 连接与超时
public void connect(SocketAddress endpoint,int timeout):将此套接字连接到服务端,并指定一个超时值。
4.3.3 获得本地端口与远程端口
public int getPort()方法的作用是返回此套接字连接到的远程端口
public int getLocalPort():返回此套接字绑定到的本地端口。
4.3.4 获得本地InetAddress地址与本地SocketAddress地址
public InetAddress getLocalAddress()方法的作用是获取套接字绑定的本地InetAddress地址信息。
public SocketAddress getLocalSocketAddress():返回此套接字绑定的端点的SocketAddress地址信息。
4.3.5 获得远程InetAddress与远程SocketAddress()地址
public InetAddress getInetAddress()方法的作用是返回此套接字连接到的远程的InetAddress地址。
public SocketAddress getRemoteSocketAddress()方法的作用是:返回此套接字远程端点的SocketAddress地址。
4.3.6 开启半读与半写状态
public void shutdownInput()方法的作用:调用此方法的一端进入半读状态,也就是此端不能获得输入流,但对端却能获得输入流。
public void shutdownOutput():禁用此套接字的输出流。
4.3.7 Socket选项TcpNoDelay
public void setTcpNoDelay(boolean on)方法的作用是启用/禁用 TCP_NODELAY
4.3.8 Socket选项SendBufferSize
public synchronized void setSendBudderSize(int size)方法的作用是将此Socket的SO_SND_BUF选项设置为指定的值。
4.4 基于UDP的Socket通信
4.4.1 使用UDP实现Socket通信
在使用UDP实现Socket通信时,服务端与客户端都是使用DatagramSocket类,传输的数据要存放在DatagramPacket类中。
DatagramSocket类表示用来发送和接收数据报包的套接字。数据报套接字是包投递服务的发送或接受点。每个在数据报套接字上发送或接收的包都是单独编制和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。在DatagramSocket上总是启用UDP广播发送。为了接收广播包,应该将DatagramSocket绑定到通配符地址。
DatagramSocket类中的public synchronized void receive(DatagramPacket p)方法的作用是从此套接字接收数据报包。
DatagramSocket类中的public void send(DatatramPacket p)方法的作用是从此套接字发送数据报包。
4.4.2 测试客户端使用UDP将字符串发送到服务端
public class Server {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(8888);
byte [] byteArray = new byte[12];
DatagramPacket myPacket = new DatagramPacket(byteArray,10);
socket.receive(myPacket);
socket.close();
System.out.println("包中数据的长度:"+myPacket.getLength());
System.out.println(new String(myPacket.getData(),0,myPacket.getLength()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
---------------------------------------
public class Client {
public static void main(String[] args) throws SocketException {
try {
DatagramSocket socket = new DatagramSocket();
socket.connect(new InetSocketAddress("localhost",8888));
String newString = "1234567890";
byte[] byteArray = newString.getBytes();
DatagramPacket myPacket = new DatagramPacket(byteArray,byteArray.length);
socket.send(myPacket);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
--------------------
运行结果:
包中数据的长度:10
1234567890
4.4.3 DatagramPacket类中常用API的使用
public synchronized void setData(byte [] buf)方法的作用是为此包设置数据缓冲区。
public synchronized void setData(byte[] buf,int offset,int length)方法的作用是为此包设置数据缓冲区。
public synchronized int getOffset():返回将要发送或接收到的数据的偏移量
public synchronized void setLength(int length) 为此包设置长度。包的长度是指包数据缓冲区中将要发送的字节数。