TCP网络编程
转载请表明出处:http://blog.csdn.net/u012637501(嵌入式_小J的天空)
从上面一节内容可以知道,利用UDP通信的两个程序是平等的,无主次之分,两个程序代码可以完全一样。但利用TCP协议进行通信的两个应用程序,是有主从之分的,一个称为服务器程序,另外一个称为客户机程序。Java中提供了ServerSocket类用于创建服务器端的socket,Socket类用于创建客户端socket。
一、APIs简介
java.net.ServerSocket
(1)功能:用于创建服务器端Socket,服务端Socket将等待网络上的客户端应用发来连接请求。
(2)构造方法
>ServerSocket() :
>ServerSocket(int port)
>ServerSocket(int port, int backlog)
>ServerSocket(int port, int backlog, InetAddress bindAddr)
用第一个构造方法创建一个ServerSocket对象,没有与任何端口号绑定,不能被直接使用,还要继续调用bind方法,才能完成其他构造方法所完成的功能。
用第二个构造方法创建一个ServerSocket对象,并与指定的端口号绑定。如果port为0,系统会应用分配一个还没有被其他网络程序所使用的端口号,作为服务器程序,端口号必须事先指定,其他客户才能根据这个端口号进行连接。
用第三个构造方法创建一个ServerSocket对象,与指定的端口号绑定并根据backlog参数指定在服务器忙时可以与之保持连接请求的等待客户数量(默认50)。
用第三个构造方法创建一个ServerSocket对象,与指定的端口号绑定、指定等待客户数量、指定本机IP。
(3)常用方法
Socket accept():服务器端等待客户端发送连接请求,并返回服务器端的Socket
void bind(SocketAddress endpoint):将服务器Socket与指定的地址(IP地址和端口号)绑定
void close():关闭该Socket
InetAddress getInetAddress():返回该服务器socket的本地地址(IP地址和端口号)
int getLocalPort():返回该服务器socket的本地端口号
java.net.Socket
(1)功能:客户端要与服务器建立连接,必须先要创建一个Socket对象。
(2)构造方法
>Socket()
>Socket(String host, int port)
>Socket(InetAddress address, int port)
>Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
>Socket(String host, int port, InetAddress localAddr, int localPort)
用第一个构造方法创建Socket对象,不与任何服务器建立连接,不能被直接使用,需要调用connect方法。
用第二或三个构造方法创建Socket对象,与指定的IP地址和端口号的服务器程序建立连接。其中,String
host为字符串格式地址,InetAddress address为InetAddress对象所包装的地址。
用第四或五个构造方法创建Socket对象,与指定的IP地址和端口号的服务器程序建立连接,并且指定了本地Socket(即客户端)所绑定的本地IP地址和端口号。
(3)常用方法
>void
connect(SocketAddress endpoint) :将该客户端socket与指定的服务器连接
>void
connect(SocketAddress endpoint, int timeout) :将该客户端socket与指定的服务器连接,并指定时限
>void
close() :关闭客户端socket
>InetAddress
getInetAddress():返回服务器程序的IP地址
>int getPort():返回服务器程序的端口号
>InputStream
getInputStream() :返回该客户端socket的一个输入流
>OutputStream
getOutputStream() :返回该客户端socket的一个输出流
二、TCP协议的Server-Client模型
1.建立Server-Client模型
(1)服务器程序创建一个ServerSocket,然后调用accept方法等待客户来连接;
(2)客户端程序创建一个Socket并请求与服务器建立连接;
(3)服务器接收客户的连接请求,并创建一个新的Socket(属于服务器)与该客户建立专线连接;
(4)刚才建立了连接的两个Socket在一个单独的线程(由服务器程序创建)上对话;
(4)服务器开始等待新的连接请求。
服务器端程序调用ServerSocket.accept方法(返回服务器的Socket)等待客户的连接请求,一旦accept接收了客户连接请求,该方法将返回一个与该客户建立了专线连接的Socket对象,而不用去创建这个服务器Socket对象。当客户端和服务器端的两个Socket建立了专线连接后,连接的一端能向另一端连续写入字节,也从另一端连续读入字节,即建立了专线连接的两个Socket是以IO流的方式进行数据交换的。Java提供了Socket.getInputStream方法返回Socket的输入流对象,Socket.getOutputStream方法返回Socket的输出流对象。只要连接的一段向该输出流对象写入了数据,连接的另一端就能从其输入流对象中读取到这些数据。
三、源码实战
1.最简单的TCP服务器程序
实现:开发一个简单的TCP服务端应用,设定其端口号,并将Windows下的telnet工具作为客户端与之通信。
ServerTCP.java
<span style="font-size:18px;">import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class ServerClient { public static void main(String[] args) { try { //1.创建一个ServerSocket对象,用于返回服务器的Socket并指定服务器的端口号 ServerSocket server = new ServerSocket(8000); Socket socket=server.accept(); //2.获取专线Socket的输入输出流 InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); //3.向输出流写入字节数据 String str = "My name is jiangdongguo.How are you!"; os.write(str.getBytes()); //4.从输入流读取数据并存储到指定的字节数组中 BufferedReader br = new BufferedReader(new InputStreamReader(is)); System.out.println(br.readLine()); // byte[] buf = new byte[1024]; // int len=is.read(buf); // System.out.println(new String(buf,0,len)); //打印实际读到的数据 socket.close(); is.close(); os.close(); server.close(); } catch (IOException e) { e.printStackTrace(); } } }</span>
源码分析:
(1)ServerSocket server = new ServerSocket(8000); 语句作用为服务端程序创建一个在8000端口上等待连接的ServerSocket对象,当接收到一个客户的连接请求后,程序从与这个客户建立了连接的Socket对象中获得输入输出流对象,通过输出流首先向客户端发送一串字符,然后通过输入流读取客户发送过来的信息,并将这些信息存放到一个字节数组中,最后关闭所有有关的资源。
(2)由于telnet工具有输入就发送,而不等回车。如有如果服务器向将接收到的数据按一行行的格式输出。Java为我们提供了一个BufferedReader类,通过该类实现按行处理输入流。
效果演示:
从下面效果可知,服务器先将数据写入Socket的输出流。当Telnet与开发的服务器连接成功后,Telent作为客户端从该socket的输入流读取数据并在终端显示。另外,我们(telnet)客户端的命令终端写入数据到socket的输出流后,服务器程序会读取socket的输入流并打印到Eclipse终端。
2.Server_Client模型应用开发
(1)服务器端
功能:在上一个程序的基础上,实现服务器程序能够接收多个客户的连接请求,并为每个客户连接创建一个单独的线程与客户进行对话。
>ServiceThread.java
子线程功能代码。每个连接的数据交换,需要放在一个循环语句中,保证两者可以不停地交换数据。客户端每向服务器发送一个字符串,服务器就将这个字符串中的所有字符反向排序后回送给客户端,直到客户端向服务器发送quit命令,结束两端的对话。
<span style="font-size:18px;">import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; //实现Runnable接口子类,完成子线程任务 public class ServiceThread implements Runnable { Socket socket; /*构造方法,传递一个Socket对象参数*/ public ServiceThread(Socket socket) { this.socket=socket; } /*成员方法*/ public void run() { try { /*-----------------------------------------------------------------------------------------*/ //1.获取Socket的输入流、输出流对象 InputStream is =socket.getInputStream(); OutputStream os=socket.getOutputStream(); //2.为输入流、输出流创建两个包装类 BufferedReader br = new BufferedReader(new InputStreamReader(is)); //输入流包装类 DataOutputStream dos = new DataOutputStream(os); //输出流包装类 /*-----------------------------------------------------------------------------------------*/ //3.该线程不停的读取输入流中的数据并倒序 while(true) { String str=br.readLine(); //从输入流中读取一行字符串 if(str.equalsIgnoreCase("quit")) break; String strReverse = (new StringBuffer(str).reverse().toString()); //将字符串倒序 dos.writeBytes(str+"------>"+strReverse+System.getProperty("line.separator")); //向输出流中写入字节数据 } br.close(); dos.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }</span>
源码分析:
(1)BufferedReader、DataOutputStream类:这两个类为IO包装类,BufferedRead类可以方便得从底层字节输入流中以整行的形式读取一个字符串;DataOutputStream类可以将一个字符串以字节数组的形式写入底层字节输出流中。
(2)System.getProperty("line.separator")用于根据不同的操作系统返回相应的换行符。
(3)在创建服务器程序Socket时需要指定其端口号(客户端才能知道哪个网络程序为服务器),然后,获取Socket的输入流、输出流用以与客户端进行数据交互。
-------------------------------------------------------------------------------------------------------------------------------------------------------
>TCPServer.java
主线程。一次accept方法调用只接受一个连接,accept方法需要方法一个循环语句中,以便接收多个客户端连接。
<span style="font-size:18px;">import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TCPServer { public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(8010);//实例化一个ServerSocket对象 while(true) { Socket socket = ss.accept(); //等待客户端连接请求,成功后返回服务器的socket new Thread(new ServiceThread(socket)).start(); //当连接成功,为socket通信创建一个线程并启动该线程 } } catch (IOException e) { e.printStackTrace(); } } }</span>
(2)客户端
>TCPClient.java
<span style="font-size:18px;">import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; public class TCPClient { public static void main(String[] args) { try { //1.创建客户端socket并指定服务器IP和端口号 Socket socket = new Socket(InetAddress.getByName("192.168.1.100"),8010); //2.获取socket的输入流、输出流 InputStream is = socket.getInputStream(); OutputStream os=socket.getOutputStream(); //3.实例化两个包装类 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); DataOutputStream dos=new DataOutputStream(os); BufferedReader bri = new BufferedReader(new InputStreamReader(is)); while(true) { String strKey = br.readLine(); //从输入流读一行数据 dos.writeBytes(strKey+System.getProperty("line.separator")); //将键盘数据写入输出流 if(strKey.endsWith("quit")) //当输入quit时,断开连接 break; else System.out.println(bri.readLine());//从输入流读取一行数据并打印 } socket.close(); br.close(); dos.close(); bri.close(); } catch (IOException e) { e.printStackTrace(); } } }</span>
源码分析:
(1)Socket socket = new Socket(InetAddress.getByName("192.168.1.100"),8010);
语句的作用是创建客户端Socket并指定服务器的IP地址和端口号。但很多时候,为了使我们的应用程序灵活性更大,可以通过命令终端来输入服务器的IP地址和端口号:
Socket socket=null;
if(args.length<2) //指定默认服务器IP和端口号
{
socket = new Socket(InetAddress.getByName("192.168.1.100"),8010);
}
else //通过命令行终端指定
{
socket=Socket((InetAddress.getByName(args[0]),Integer.parseInt(args[1]));
}
注:上述方法对服务器设置端口为一致思想。
效果演示1:
这里使用Telnet作为客户端,观察下面结果我们可以看出,在命令行运行多个"Telnet
服务器IP 服务器端口号"后,客户端程序与服务端在一个单独的线程中建立连接,实现数据交换。
效果演示2:
这里使用我们编写好的应用作为客户端,每一个客户都可以同服务器单独对话,直到客户输入quit命令后结束。其效果与telnet一致。