Socket:英文原义是“孔”或“插座”,在这里作为4BDS UNIX的进程通信机制。socket非常类似于电话插座,以一个国家级电话网为例:电话的通话双方相当于相互通信的2个进程,区号是它的网络地址,区内的交换机相当于主机,主机分配给每个用户局内的号码相当于socket号。任何用户在通话之前,首先要占有一部电话机,相当于申请一个socket,同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤消连接。
一、分类:
1、通信方式
1>一个客户端连接一个服务器,或称为“点对点”(peer to peer);
2>一个客户端连接多个服务器,这种方式很少见,主要用于一个客户端向多个服务器发送请求;
3>多个客户端连接一个服务器,这就是最常见的方式;
2、连接方式
1>短连接
连接—>传输数据—>关闭连接,也就是说当数据传输完之后会马上将连接关闭;一般在客户端数量较多而连接又比较频繁时采用短连接,常用于多个客户端连接一个服务器。
2>长连接
连接—>传输数据—>保持连接—>传输数据—>保持连接……—>关闭连接,也就是说当数据传输完之后不会马上将连接关闭,而继续用该连接传输后续的数据。一般在客户端数量较少但连接又比较频繁时采用长连接,常用于点对点通信。
3、传输方式
1>单工
信息的传输方向是单向的。在这种模式下,通信过程中只有一个设备担任发送的角色,而另一个设备则是接收端。信息只能从一个方向流向另一个方向,不存在双向通信的能力。单工通信广泛应用于一些特定的场景,如广播、遥控器控制以及某些工业自动化的传感器和执行器通信。
2>半双工
信息的传输方向是双向的,但对于任一个端不能同时进行发送和接收,只能要么发送,要么接收。在这种模式下,通信过程中的两方设备都可以担任发送和接收的角色,一方发送,另一方接收。但如果此时充当了发送的角色,就不能再允当接收的角色。
3>全双工
信息的传输方向是双向的,双端可以同时进行发送和接收。在这种模式下,通信过程中的两方设备都可以担任发送和接收的角色,发送的同时也可以接收。
4、发送接收方式
1>异步
报文发送和接收是分开的,相互独立的,互不影响。
2>同步
发送报文和接收报文是同步进行的,即报文发送后等待接收返回报文。同步方式一般需要考虑超时问题,即报文发出后不能无限等待,需要设定超时时间,超过了这个时间发送方就不再等待读返回报文,直接通知超时返回。
5、读取报文的方式
1>阻塞与非阻塞方式
①阻塞式:
如果没有报文接收到,则读函数一直处于等待状态,直到有报文到达。
②非阻塞式:
读函数不停地进行读动作,如果没有接收到报文,等待一段时间后超时返回,这种情况一般需要指定超时时间。
2>一次性读写与循环读写方式
①一次性读写:
在接收或发送报文动作中一次性不加分割地全部读取或全部发送报文字节。
②不指定长度循环读写:
一般发生在短连接中,受网络路由等限制,一次较长的报文可能在网络传输过程中被分解成了好几个包。一次读取可能不能全部读完一次报文,这就需要循环读报文,直到读完为止。
③带长度报文头的循环读写方式:
这种情况一般是在长连接中,由于在长连接中没有条件能够判断循环读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读报文。实际情况中,报文头的码制格式还经常不一样,如果是非ASCII码的报文头,还必须转换成ASCII。常见的报文头码制有:
❶ASCII码;
❷BCD码;
二、服务端:
1、构造方法
在客户/服务器通信模式中, 服务器端需要创建监听端口的ServerSocket,ServerSocket负责接收客户端的连接请求。常用构造方法有:
ServerSocket() throws IOException:该方法创建ServerSocket时不与任何端口绑定,而之后需要通过bind()方法与特定端口绑定。用途是:允许服务器在绑定到特定端口之前,先设置ServerSocket的一些属性。因为一旦服务器与特定端口绑定,有些属性就不能再改变了。
ServerSocket(int port) throws IOException
ServerSocket(int port, int backlog) throws IOException
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
参数port指定服务器要绑定的端口(服务器要监听的端口),参数backlog指定客户端连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。
2、ServerSocket 属性
ServerSocket主要有以下属性:
1>SO_TIMEOUT:表示等待客户连接的超时时间。表示accept()方法等待客户连接的超时时间,以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。
2>SO_REUSEADDR:表示是否允许重用服务器所绑定的地址,即:是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口。
3>SO_RCVBUF:表示接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(基于HTTP 或 FTP 协议的数据传输) 可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互频繁且单次传送数量比较小的通信(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
无论在 ServerSocket绑定到特定端口之前或之后,调用 setReceiveBufferSize() 方法都有效。例外情况下是如果要设置大于 64 KB 的缓冲区,则必须在 ServerSocket 绑定到特定端口之前进行设置才有效。
4>性能偏好:见客户端属性。
3、绑定端口
如果在构造方法中未绑定端口,可以使用如下方法进行绑定:
public void bind(SocketAddress endpoint) throws IOException
如果运行时无法绑定到端口,会抛出IOException,更确切地说是抛出 BindException,它是SocketException的子类。BindException一般是由以下原因造成的:
1>端口已经被其他服务器进程占用;
2>在某些操作系统中,保留端口(0-1023)必须以超级管理员来运行,非超级管理员不能运行;
如果把参数port设为0,表示由操作系统来为服务器分配一个任意可用的端口,由操作系统分配的端口也称为匿名端口。对于多数服务器,会使用明确的端口,而不会使用匿名端口,因为客户程序需要事先知道服务器的端口,才能方便地访问服务器。但在某些场合,匿名端口有着特殊的用途。比如FTP(文件传输协议)就使用了匿名端口,FTP协议用于在本地文件系统与远程文件系统之间传送文件。FTP使用两个并行的TCP连接:一个是控制连接,一个是数据连接。控制连接用于在客户端和服务器之间发送控制信息,如用户名和口令、改变远程目录的命令或上传和下载文件的命令。数据连接用于传送文件。TCP服务器在21端口上监听控制连接,如果有客户要求上传或下载文件,就另外建立一个数据连接,通过它来传送文件。数据连接的建立有两种方式:
①TCP服务器在20 端口上监听数据连接,TCP客户主动请求建立与该端口的连接。
②首先由TCP 客户端创建一个监听匿名端口的ServerSocket,再把这个 ServerSocket 监听的端口号发送给TCP服务器,然后由TCP 服务器主动请求建立与客户端的连接,这种方式就使用了匿名端口。
4、设置客户连接请求队列的长度
backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。值得注意的是,在以下几种情况中,仍然会采用操作系统限定的队列的最大长度:
1>backlog参数的值大于操作系统限定的队列的最大长度;
2>backlog参数的值小于或等于0;
3>在ServerSocket构造方法中没有设置backlog参数。
当服务器进程运行时,可能会同时监听到多个客户的连接请求。例如,每当一个客户端进程执行以下代码:
Socket socket = new Socket("www.baidu.com", 80);
就意味着在远程www.baidu.com主机的80端口上监听到了一个客户端的连接请求。管理客户端连接请求的任务是由操作系统来完成的。操作系统把这些连接请求存储在一个先进先出的队列中。许多操作系统限定了队列的最大长度,一般为50。当队列中的连接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求使队列腾出空位时,队列才能继续加入新的连接请求。
对于客户端进程,如果它发出的连接请求被加入到服务器的请求连接队列中,就意味着客户与服务器的连接建立成功,客户端进程从Socket构造方法中正常返回。如果客户端发出的连接请求被服务端拒绝,Socket构造方法就会抛出ConnectionException。
注意:创建绑定端口的服务端进程后,当客户端进程的Socket构造方法返回成功,表示客户进程的连接请求被加入到服务器进程的请求连接队列中。虽然客户端成功返回Socket对象,但是还没跟服务器进程形成一条通信线路。必须在服务端进程通过ServerSocket的accept()方法从请求连接队列中取出连接请求,并返回一个Socket对象后,服务端进程这个Socket对象才与客户端的Socket对象形成一条通信线路。
5、绑定IP地址
如果服务器只有一个IP地址,那么默认情况下,服务器程序就与该IP地址绑定。ServerSocket的构造方法ServerSocket(int port, int backlog, InetAddress bingAddr)中有一个bindAddr参数,它显式指定服务器要绑定的IP地址,该构造方法适用于具有多个IP地址的服务器。假定一个主机有两个网卡,一个网卡用于连接到Internet,IP为222.67.5.94,还有一个网卡用于连接到本地局域网,IP地址为192.168.3.4。如果服务器仅仅被本地局域网中的客户访问,那么可以按如下方式创建ServerSocket:
ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName("192.168.3.4"));
6、获取ServerSocket的信息
1>public InetAddress getInetAddress();获取服务器绑定的IP地址
2>public int getLocalPort();获取服务器绑定的端口
7、接收客户端的信息
public Socket accept();从连接请求队列中取出一个客户端的连接请求,然后创建与客户连接的Socket对象,并将它返回。如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。接下来服务器从Socket对象中获得输入流和输出流,就能与客户交换数据。当服务端正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常:java.net.SocketException:Connection reset by peer。这只是服务器与单个客户通信中出现的异常, 这种异常应该被捕获, 使得服务器能继续与其他客户通信。
8、关闭ServerSocket
public void close();使服务端进程释放占用的端口,并且断开与所有客户端的连接。当一个服务端进程运行结束时,即使没有执行ServerSocket的close()方法,操作系统也会释放这个服务端进程占用的端口,因此服务端不是必须要在结束之前执行该方法。
在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket的close()方法。例如,以下代码用于扫描1-65535之间的端口号。如果ServerSocket成功创建,意味这该端口未被其他服务器进程绑定,否则说明该端口已经被其他进程占用:
for(int port=1; port<=65535; port++){
try{
ServerSocket serverSocket = new ServerSocket(port);
serverSocket.close(); //及时关闭ServerSocket
}catch(IOException e){
System.out.println("端口" + port + " 已经被其他服务器进程占用");
}
}
ServerSocket的isClosed()方法判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则即使ServerSocket还没有和特定端口绑定,isClosed()也会返回false。
ServerSocket的isBound()方法判断ServerSocket是否已经与端口绑定,只要ServerSocket已经与端口绑定,即使它已经被关闭,isBound()方法也会返回true。
当ServerSocket关闭后,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立即释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。
三、客户端:
1、构造方法
1>Socket();
2>Socket(InetAddress address, int port);
3>Socket(InetAddress address, int port, InetAddress localAddr, int localPort);
4>Socket(String host, int port);
5>Socket(String host, int port, InetAddress localAddr, int localPort);
除第一个不带参数的构造方法以外,其他构造方法都会试图建立与服务器的连接,如果连接成功,就返回Socket对象。如果因为某些原因连接失败就会抛出IOException。
2、Socket 属性
Socket主要有以下属性:
1>TCP_NODELAY:默认情况下发送数据采用Negale算法。Negale算法是指发送方发送的数据不会立即发出,而是先放在缓冲区,等缓存区满了再发出。发送完一批数据后,会等待接收方对这批数据的回应,然后再发送下一批数据。Negale算法适用于发送方需要发送大批量数据,并且接收方会及时作出回应的场合,这种算法通过减少传输数据的次数来提高通信效率。如果发送方持续地发送小批量的数据,并且接收方不一定会立即发送响应数据,那么Negale算法会使发送方运行很慢。对于GUI程序,如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动),这个问题尤其突出。客户端鼠标位置改动的信息需要实时发送到服务器上,由于Negale算法采用缓冲,大大减低了实时响应速度,导致客户程序运行很慢。TCP_NODELAY的默认值为false,表示采用Negale算法。如果调用setTcpNoDelay(true)方法,就会关闭Socket的缓冲确保数据及时发送。如果Socket的底层实现不支持TCP_NODELAY选项,那么getTcpNoDelay()和setTcpNoDelay方法会抛出SocketException。
2>SO_REUSEADDR:当接收方通过Socket的close()方法关闭Socket时,如果网络上还有发送到这个Socket的数据,那么底层的Socket不会立即释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。Socket接收到延迟数据后,不会对这些数据作任何处理。Socket接收延迟数据的目的是,确保这些数据不会被其他碰巧绑定到同样端口的新进程接收到。客户端程序一般采用随机端口,因此出现两个客户程序绑定到同样端口的可能性不大。但服务器程序都使用固定的端口,当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,启动失败。为了确保一个进程关闭Socket后,即使它还没释放端口,同一个主机上的其他进程还可以立即重用该端口,可以调用Socket的setReuseAddress(true)方法。该方法必须在Socket还没有绑定到一个本地端口之前调用,否则执行setRsuseAddress(true)方法无效。因此必须按照以下方式创建Socket对象,然后再连接远程服务器:
Socket socket = new Socket();
socket.setReuseAddress(true);
SocketAddress remoteAddr = new InetSocketAddress("www.baidu.com",8000);
//绑定本地的匿名端口并且连接到远程服务器
socket.connect(remoteAddr);
或者:
Socket socket = new Socket();
socket.setReuseAddress(true);
SocketAddress localAddr = new InetSocketAddress("localhost",9000);
SocketAddress remoteAddr = new InetSocketAddress("www.baidu.com",8000);
//绑定本地端口
socket.bind(localAddr);
//连接远程服务器
socket.connect(remoteAddr);
此外两个共用同一个端口的进程必须都调用setReuseAddress(true)方法,才能使得一个进程关闭Socket后,另一个进程的Socket能够立即重用相同端口。
3>SO_TIMEOUT:用于设定接收数据的等待超时时间,单位为毫秒,它的默认值为0,表示会无限等待永远不会超时。Socket的setSoTimeout()方法必须在接收数据之前执行才有效。此外当输入流的read()方法抛出SocketTimeoutException后Socket仍然是连接的,可以尝试再次读数据。
4>SO_LINGER:用来控制Socket关闭时的行为。默认情况下,如果未设置SO_LINGER选项,getSoLinger()返回的结果是-1,执行Socket的close()方法,该方法会立即返回,但底层的Socket实际上并不立即关闭,它会延迟一段时间,直到发送完所有剩余的数据,才会真正关闭Socket,断开连接。如果执行以下方法:
socket.setSoLinger(true, 3600);
getSoLinger()返回的结果是3600,当执行Socket的close()方法,该方法不会立即返回,而是进入阻塞状态。同时底层的Socket会尝试发送剩余的数据。只有满足以下两个条件之一,close()方法才返回:
①底层的Socket已经发送完所有的剩余数据;
②底层的Socket还没有发送完所有的剩余数据,但超过3600秒,剩余未发送的数据被丢弃然后close()方法返回。
在以上两种情况中,当close()方法返回后,底层的Socket会被关闭,断开连接。此外setSoLinger(booleanon,intseconds)方法中的seconds参数以秒为单位,而不是以毫秒为单位。
5>SO_RCVBUF:表示Socket接收数据的缓冲区大小。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的通信)可以使用较大的缓冲区,这可以减少传输数据的次数,提高传输数据的效率。而对于交互频繁且单次传送数据量比较小的通信方式(Telnet和网络游戏),则应该采用小的缓冲区,确保小批量的数据能及时发送给对方。这种设定缓冲区大小的原则也同样适用于Socket的SO_SNDBUF选项。如果底层Socket不支持 SO_RCVBUF 选项,那么setReceiveBufferSize()方法会抛出 SocketException。
6>SO_SNDBUF:表示Socket发送数据的缓冲区大小。如果底层 Socket 不支持 SO_SNDBUF 选项,setSendBufferSize()方法会抛出SocketException。
7>SO_KEEPALIVE:当SO_KEEPALIVE选项为true时,表示底层的TCP实现会监视该连接是否有效。当连接处于空闲状态(连接的两端没有互相传送数据)超过了2小时时,本地的TCP实现会发送一个数据包给远程的Socket。如果远程Socket没有发回响应,TCP实现就会持续尝试11分钟,直到接收到响应为止。如果在12分钟内未收到响应,TCP实现就会自动关闭本地Socket,断开连接。在不同的网络平台上,TCP实现尝试与远程Socket对话的时限有所差别。SO_KEEPALIVE选项的默认值为false,表示TCP不会监视连接是否有效,不活动的客户端可能会永远存在下去,而不会关注服务端是否已关闭。
8>OOBINLINE:OOBINLINE的默认值为false,在这种情况下,当接收方收到紧急数据时不作任何处理,直接将其丢弃。如果用户希望发送紧急数据,应该把OOBINLINE设为true。当OOBINLINE为true时,表示支持发送一个字节的TCP紧急数据。Socket类的sendUrgentData(int data)方法用于发送一个字节的TCP紧急数据。
socket.setOOBInline(true);
接收方会把接收到的紧急数据与普通数据放在同样的队列中。值得注意的是,除非使用一些更高层次的协议,否则接收方处理紧急数据的能力有限,当紧急数据到来时,接收方不会得到任何通知,因此接收方很难区分普通数据与紧急数据,只好按照同样的方式处理它们。
9>服务类型:
IP规定了4种服务类型,用来定性地描述服务的质量,Socket 类用 4 个整数表示服务类型:
①低成本(0x02):发送成本低,耗费资源少;
②高可靠性(0x04):保证把数据可靠地送达目的地;
③最高吞吐量(0x08):一次可以接收或发送大批量的数据;
④最小延迟(0x10):传输数据的速度快, 把数据快速送达目的地。
socket.setTrafficClass(0x04);
这4种服务类型还可以进行组合。例如可以同时要求获得高可靠性和最小延迟。
socket.setTrafficClass(0x04|0x10);
10>性能偏好:
默认情况下套接字使用TCP/IP协议。有些实现可能提供与TCP/IP具有不同性能特征的替换协议。此方法允许应用程序在实现从可用协议中作出选择时表达它自己关于应该如何进行折衷的偏好。
socket.setPerformancePreferences(int connectionTime,int latency,int bandwidth);
①connectionTime:表示用最少时间建立连接;
②latency:表示最小延迟;
③bandwidth:表示最高带宽。
setPerformancePreferences()方法用来设定这3项指标之间的相对重要性。可以为这些参数赋予任意的整数,较大的值指示更强的偏好。负值表示的优先级低于正值。例如,如果应用程序相对于低延迟和高带宽更偏好短连接时间,则其可以使用值 (1, 0, 0) 调用此方法。如果应用程序相对于低延迟更偏好高带宽,而相对于短连接时间更偏好低延迟,则其可以使用值 (0, 1, 2) 调用此方法。 另外在连接套接字后调用此方法无效。
四、示例:
1、不使用线程,实现客户端与服务端单点通信
Server:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
Socket socket = null;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
System.out.println("正在启动服务器...");
server = new ServerSocket(4000);
System.out.println("已启动服务器,等待客户端请求...");
// 一直是阻塞状态,除非客户端连接请求
socket = server.accept();
System.out.println("客户端已连接到本服务器...");
// 用于接收客户端的请求
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 用于返回给客户端
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 从客户端接收到的字符串
String receiveStr;
while (!"over".equals(receiveStr = bufferedReader.readLine())) {
System.out.println("服务端收到客户端发送的内容是:" + receiveStr);
// 做一下简单的转化处理,由服务端返回给客户端
String sendStr = "$" + receiveStr + "$";
System.out.println("服务端返回给客户端的内容是:" + sendStr);
bufferedWriter.write(sendStr);
// 客户端调用readLine()方法必须有换行标识才能不阻塞
bufferedWriter.newLine();
// 使用缓冲流时如果缓冲没有满则不去写,除非使用flush()方法
bufferedWriter.flush();
}
System.out.println("服务端读取结束...");
bufferedReader.close();
bufferedWriter.close();
socket.close();
server.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bufferedWriter) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != socket) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != server) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Client:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.io.IOException;
public class Client {
public static void main(String[] args) {
BufferedReader inputBufferedReader = null;
BufferedWriter bufferedWriter = null;
BufferedReader receiveBufferedReader = null;
Socket client = null;
try {
System.out.println("正在连接服务器...");
client = new Socket("127.0.0.1", 4000);
System.out.println("已连接到服务器...");
// 用于接收控制台的输入
inputBufferedReader = new BufferedReader(new InputStreamReader(System.in));
// 用于发送给服务端
bufferedWriter = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
// 用于接收服务端的返回
receiveBufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
// 发送到服务端的字符串
String sendStr;
// 从服务端接收到的字符串
String receiveStr;
do {
System.out.println("请输入要发送给服务端的内容:");
sendStr = inputBufferedReader.readLine();
System.out.println("客户端输入的内容是:" + sendStr);
bufferedWriter.write(sendStr);
// 服务端调用readLine()方法必须有换行标识才能不阻塞
bufferedWriter.newLine();
// 使用缓冲流时如果缓冲没有满则不去写,除非使用flush()方法
bufferedWriter.flush();
if (!"over".equals(sendStr)) {
receiveStr = receiveBufferedReader.readLine();
System.out.println("接收到服务端发送过来的内容是:" + receiveStr);
}
} while (!"over".equals(sendStr));
System.out.println("客户端发送结束...");
inputBufferedReader.close();
bufferedWriter.close();
receiveBufferedReader.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != inputBufferedReader) {
try {
inputBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bufferedWriter) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != receiveBufferedReader) {
try {
receiveBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
大多数情况下会有多个客户端向服器发送请求,各个客户端之间互不影响,也就是说当某客户端发生异常或阻塞时不会影响其他客户端向服务端发送消息,因此需要使用多线程。
2、使用原始多线程,实现多个客户端与服务器端通信。
Server:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server extends Thread {
private static ServerSocket server;
private Socket socket;
public Server(Socket socket) {
this.socket = socket;
}
public void run() {
System.out.println("服务端已为该客户端分配线程:" + this.getName());
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream(), "UTF-8"));
bufferedWriter = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream(), "UTF-8"));
String receiveStr;
while (!"over".equals(receiveStr = bufferedReader.readLine())) {
System.out.println("服务端收到客户端发送的内容是:" + receiveStr);
// 做一下简单的转化处理,由服务端返回给客户端
String sendStr = "$" + receiveStr + "$";
System.out.println("服务端返回给客户端的内容是:" + sendStr);
bufferedWriter.write(sendStr);
// 客户端调用readLine()方法必须有换行标识才能不阻塞
bufferedWriter.newLine();
// 使用缓冲流时如果缓冲没有满则不去写,除非使用flush()方法
bufferedWriter.flush();
}
System.out.println("服务端读取结束...");
bufferedReader.close();
bufferedWriter.close();
this.socket.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bufferedWriter) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != this.socket) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("线程:" + this.getName() + "已结束");
}
}
public static void main(String[] args) throws IOException {
if (args.length == 1) {
int serverPort = Integer.parseInt(args[0]);
System.out.println("正在启动服务器...");
server = new ServerSocket(serverPort);
System.out.println("服务器已启动,等待客户端请求...");
while (true) {
Socket socket = server.accept();
System.out.println("服务端收到客户端地址:" + socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + "的请求...");
Server serverThread = new Server(socket);
serverThread.start();
}
} else {
System.out.println("输入内容格式不正确,即将退出...");
System.exit(1);
}
}
}
Client:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.io.IOException;
public class Client {
public static void main(String[] args) {
if (args.length!=1) {
System.out.println("输入内容格式不正确,即将退出...");
System.exit(1);
}
int serverPort = Integer.parseInt(args[0]);
BufferedReader inputBufferedReader = null;
BufferedWriter bufferedWriter = null;
BufferedReader receiveBufferedReader = null;
Socket client = null;
try {
System.out.println("正在连接服务器...");
client = new Socket("127.0.0.1", serverPort);
System.out.println("已连接到服务器...");
// 用于接收控制台的输入
inputBufferedReader = new BufferedReader(new InputStreamReader(System.in));
// 用于发送给服务端
bufferedWriter = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
// 用于接收服务端的返回
receiveBufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
// 发送到服务端的字符串
String sendStr;
// 从服务端接收到的字符串
String receiveStr;
do {
System.out.println("请输入要发送给服务端的内容:");
sendStr = inputBufferedReader.readLine();
System.out.println("客户端输入的内容是:" + sendStr);
bufferedWriter.write(sendStr);
// 服务端调用readLine()方法必须有换行标识才能不阻塞
bufferedWriter.newLine();
// 使用缓冲流时如果缓冲没有满则不去写,除非使用flush()方法
bufferedWriter.flush();
if (!"over".equals(sendStr)) {
receiveStr = receiveBufferedReader.readLine();
System.out.println("接收到服务端发送过来的内容是:" + receiveStr);
}
} while (!"over".equals(sendStr));
System.out.println("客户端发送结束...");
inputBufferedReader.close();
bufferedWriter.close();
receiveBufferedReader.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != inputBufferedReader) {
try {
inputBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bufferedWriter) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != receiveBufferedReader) {
try {
receiveBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
使用原始的多线程需要频繁地创建线程,每有一个客户端请求就需要创建一个线程,造成不必要的资源浪费。因此需要使用线程池,可以循环利用线程而不需要频繁创建线程耗费系统资源。
3、使用线程池,实现多个客户端与服务器端通信。
Server:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server extends Thread {
private static ServerSocket server;
private Socket socket;
public Server(Socket socket) {
this.socket = socket;
}
public void run() {
System.out.println("服务端已为该客户端分配线程:" + this.getName());
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream(), "UTF-8"));
bufferedWriter = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream(), "UTF-8"));
String receiveStr;
while (!"over".equals(receiveStr = bufferedReader.readLine())) {
System.out.println("服务端收到客户端发送的内容是:" + receiveStr);
// 做一下简单的转化处理,由服务端返回给客户端
String sendStr = "$" + receiveStr + "$";
System.out.println("服务端返回给客户端的内容是:" + sendStr);
bufferedWriter.write(sendStr);
// 客户端调用readLine()方法必须有换行标识才能不阻塞
bufferedWriter.newLine();
// 使用缓冲流时如果缓冲没有满则不去写,除非使用flush()方法
bufferedWriter.flush();
}
System.out.println("服务端读取结束...");
bufferedReader.close();
bufferedWriter.close();
this.socket.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bufferedWriter) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != this.socket) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("线程:" + this.getName() + "已结束");
}
}
public static void main(String[] args) throws IOException {
if (args.length == 1) {
int serverPort = Integer.parseInt(args[0]);
System.out.println("正在启动服务器...");
server = new ServerSocket(serverPort);
System.out.println("服务器已启动,等待客户端请求...");
ExecutorService executorService = Executors.newFixedThreadPool(2);
while (true) {
Socket socket = server.accept();
System.out.println("服务端收到客户端地址:" + socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + "的请求...");
Server serverThread = new Server(socket);
executorService.execute(serverThread);
}
} else {
System.out.println("输入内容格式不正确,即将退出...");
System.exit(1);
}
}
}
Client:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.io.IOException;
public class Client {
public static void main(String[] args) {
if (args.length!=1) {
System.out.println("输入内容格式不正确,即将退出...");
System.exit(1);
}
int serverPort = Integer.parseInt(args[0]);
BufferedReader inputBufferedReader = null;
BufferedWriter bufferedWriter = null;
BufferedReader receiveBufferedReader = null;
Socket client = null;
try {
System.out.println("正在连接服务器...");
client = new Socket("127.0.0.1", serverPort);
System.out.println("已连接到服务器...");
// 用于接收控制台的输入
inputBufferedReader = new BufferedReader(new InputStreamReader(System.in));
// 用于发送给服务端
bufferedWriter = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
// 用于接收服务端的返回
receiveBufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
// 发送到服务端的字符串
String sendStr;
// 从服务端接收到的字符串
String receiveStr;
do {
System.out.println("请输入要发送给服务端的内容:");
sendStr = inputBufferedReader.readLine();
System.out.println("客户端输入的内容是:" + sendStr);
bufferedWriter.write(sendStr);
// 服务端调用readLine()方法必须有换行标识才能不阻塞
bufferedWriter.newLine();
// 使用缓冲流时如果缓冲没有满则不去写,除非使用flush()方法
bufferedWriter.flush();
if (!"over".equals(sendStr)) {
receiveStr = receiveBufferedReader.readLine();
System.out.println("接收到服务端发送过来的内容是:" + receiveStr);
}
} while (!"over".equals(sendStr));
System.out.println("客户端发送结束...");
inputBufferedReader.close();
bufferedWriter.close();
receiveBufferedReader.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != inputBufferedReader) {
try {
inputBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bufferedWriter) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != receiveBufferedReader) {
try {
receiveBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4、使用线程池和XML报文,实现多个客户端与服务器端通信。
Message:
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
@XmlRootElement
@XmlType(propOrder = {"id", "title", "content"})
class Body {
private String id;
private String title;
private String content;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static String beanToXml(Body bean) {
System.out.println("原始对象信息:\nid=" + bean.getId() + ", title=" + bean.getTitle() + ", content=" + bean.getContent());
String xmlStr = "";
StringWriter stringWriter = new StringWriter();
try {
JAXBContext context = JAXBContext.newInstance(bean.getClass());
Marshaller marshaller = context.createMarshaller();
// 字符编码
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
// 省略xml头信息<?xml version="1.0"?>
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, false);
// 是否格式化xml字符串
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(bean, stringWriter);
xmlStr = stringWriter.toString();
} catch (Exception e) {
e.printStackTrace();
}
return xmlStr;
}
public static Body xmlToBean(String xmlStr) {
System.out.print("原始对象xml信息:\n" + xmlStr);
// 去除换行符
Body bean = null;
try {
JAXBContext context = JAXBContext.newInstance(Body.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
bean = (Body) unmarshaller.unmarshal(new StringReader(xmlStr));
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}
}
public class Message {
private String chkCode; // 同步校验位(2个字节)
private String md5; // 报文体MD5校验位(32个字节)
private int msgLen; // 报文总长度(4个字节)
private String body; // 报文体(最多512个字节)
public Message(String chkCode, Body body) throws Exception {
// 同步校验位
System.out.println("构造消息chkCode=" + chkCode);
int chkCodeLength = chkCode.getBytes().length;
if (chkCodeLength != 2) {
throw new Exception("chkCode长度错误");
}
// 报文体字符串
String xmlStr = Body.beanToXml(body);
System.out.print("构造消息body=\n" + xmlStr);
int bodyLength = xmlStr.getBytes().length;
if (bodyLength > 512) {
throw new Exception("报文体长度超长");
}
// md5值
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5Bytes = messageDigest.digest(xmlStr.getBytes());
int md5BytesLength = md5Bytes.length;
if (md5BytesLength != 16) {
throw new Exception("md5值长度错误");
}
String md5 = new BigInteger(1, md5Bytes).toString(16);
System.out.println("构造消息md5=" + md5);
this.chkCode = chkCode;
this.body = xmlStr;
this.md5 = md5;
// 消息总长度
this.msgLen = 2 + 32 + 4 + bodyLength;
System.out.println("构造消息msgLen=" + this.msgLen);
}
public byte[] getBytes() {
// 消息一共需要的字节数
byte[] bytes = new byte[this.msgLen];
// 0-1个字节存放同步校验位
byte[] chkCodeBytes = this.chkCode.getBytes();
bytes[0] = chkCodeBytes[0];
bytes[1] = chkCodeBytes[1];
// 2-33个字节存放报文体MD5校验位
byte[] md5Bytes = this.md5.getBytes();
for (int i = 0; i < md5Bytes.length; i++) {
bytes[2 + i] = md5Bytes[i];
}
// 34-37个字节存放报文总长度
bytes[34] = (byte) ((this.msgLen >> 24) & 0xff);
bytes[35] = (byte) ((this.msgLen >> 16) & 0xff);
bytes[36] = (byte) ((this.msgLen >> 8) & 0xff);
bytes[37] = (byte) (this.msgLen & 0xff);
// 剩余字节存放报文体
byte[] bodyBytes = this.body.getBytes();
for (int i = 0; i < bodyBytes.length; i++) {
bytes[38 + i] = bodyBytes[i];
}
return bytes;
}
}
Server:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server extends Thread {
private static ServerSocket server;
private Socket socket;
public Server(Socket socket) {
this.socket = socket;
}
public void run() {
System.out.println("服务端已为该客户端分配线程:" + this.getName());
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = this.socket.getInputStream();
outputStream = this.socket.getOutputStream();
byte[] receiveBytes = new byte[550];
int readLength = inputStream.read(receiveBytes);
byte[] chkCodeBytes = Arrays.copyOfRange(receiveBytes, 0, 2);
System.out.println("服务端收到的chkCode是:" + new String(chkCodeBytes));
byte[] md5Bytes = Arrays.copyOfRange(receiveBytes, 2, 34);
System.out.println("服务端收到的md5是:" + new String(md5Bytes));
byte[] msgLenBytes = Arrays.copyOfRange(receiveBytes, 34, 38);
int msgLen = (msgLenBytes[0] & 0xff) << 24 | (msgLenBytes[1] & 0xff) << 16 | (msgLenBytes[2] & 0xff) << 8 | (msgLenBytes[3] & 0xff);
System.out.println("服务端收到的msgLen是:" + msgLen);
byte[] bodyBytes = Arrays.copyOfRange(receiveBytes, 38, readLength);
String xmlStr = new String(bodyBytes);
System.out.print("服务端收到的xml字符串是:\n" + xmlStr);
Body bean = Body.xmlToBean(xmlStr);
System.out.println("服务端收到的对象:\nid=" + bean.getId() + ", title=" + bean.getTitle() + ", content=" + bean.getContent());
// 服务端返回给客户端ok字符串
outputStream.write("ok".getBytes());
outputStream.flush();
System.out.println("服务端读取结束...");
inputStream.close();
outputStream.close();
this.socket.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != this.socket) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("线程:" + this.getName() + "已结束");
}
}
public static void main(String[] args) throws IOException {
if (args.length == 1) {
int serverPort = Integer.parseInt(args[0]);
System.out.println("正在启动服务器...");
server = new ServerSocket(serverPort);
System.out.println("服务器已启动,等待客户端请求...");
ExecutorService executorService = Executors.newFixedThreadPool(2);
while (true) {
Socket socket = server.accept();
System.out.println("服务端收到客户端地址:" + socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + "的请求...");
Server serverThread = new Server(socket);
executorService.execute(serverThread);
}
} else {
System.out.println("输入内容格式不正确,即将退出...");
System.exit(1);
}
}
}
Client:
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("输入内容格式不正确,即将退出...");
System.exit(1);
}
int serverPort = Integer.parseInt(args[0]);
Socket client = null;
BufferedReader inputBufferedReader = null;
OutputStream outputStream = null;
InputStream inputStream = null;
try {
System.out.println("正在连接服务器...");
client = new Socket("127.0.0.1", serverPort);
System.out.println("已连接到服务器...");
// 用于接收控制台的输入
inputBufferedReader = new BufferedReader(new InputStreamReader(System.in));
outputStream = client.getOutputStream();
inputStream = client.getInputStream();
// 是否结束标记
String flag;
do {
System.out.println("准备发送报文...");
Body body = new Body();
System.out.println("请输入id:");
body.setId(inputBufferedReader.readLine());
System.out.println("请输入title:");
body.setTitle(inputBufferedReader.readLine());
System.out.println("请输入content:");
body.setContent(inputBufferedReader.readLine());
// 消息校验码
String chkCode = "AE";
Message message = new Message(chkCode, body);
System.out.println("客户端发送的报文是:" + message);
outputStream.write(message.getBytes());
outputStream.flush();
// 服务端返回的信息
byte[] receiveBytes = new byte[10];
inputStream.read(receiveBytes);
System.out.println("服务端返回的报文是:" + new String(receiveBytes));
System.out.println("是否继续发送报文?y or n");
flag = inputBufferedReader.readLine();
} while ("y".equals(flag));
System.out.println("报文发送结束...");
inputBufferedReader.close();
outputStream.close();
inputStream.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != inputBufferedReader) {
try {
inputBufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}