上篇文章《弄懂UDP与TCP协议,这一篇就够了》我们已经初步了解网络编程的相关知识,也了解了UDP协议与TCP协议的相关内容。这种面向连接的TCP协议就是在正式通信前必须要与对方建立起连接,是按照电话系统建模的。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。这篇文章具体讲解TCP编程。
一.电脑之间如何传输数据?
如果有两台电脑,它们之间传输数据就是通过七层协议,输出端(电脑A)输出数据时要通过七层协议,数据传输给输入端(电脑B)时也要经过七层协议。具体如下图所示:
但是从应用层到传输层,计算机是如何直到要用哪个协议,是TCP还是UDP。其实在两层之间有一个套接字(Socket),可以通过socket知道要用哪个协议。
在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。
二.单向通信
第一步:建立连接
在Java API中以java.net.Socket类的对象代表网络连接,所以建立客户端网络连接,也就是创建Socket类型的对象,该对象代表网络连接,示例如下:
Socket socketA = new Socket(“192.168.1.103”,8888);
Socket socketB = new Socket(“www.baidu.com”,1000);
socketA表示连接到IP地址为192.168.1.103的计算机的8888号端口;
socketB表示连接到域名为www.baidu.com的计算机的1000号端口。
第二步:网络数据交换
在Java语言中,数据传输功能由Java IO实现,也就是说只需要从连接中获得输入流和输出流即可,然后将需要发送的数据写入连接对象的输出流中,在发送完成以后从输入流中读取数据即可。示例代码如下:
OutputStream os = socket1.getOutputStream(); //获得输出流
InputStream is = socket1.getInputStream(); //获得输入流
第三步:关闭网络,流
socket1.close();
举例:客户端向服务端发送信息
客户端:
package com.Stream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientTest//客户端
{
public static void main(String[] args) throws UnknownHostException, IOException {
//第一步
Socket client = new Socket("localhost",9999);
//第二步
OutputStream os = client.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("你好");
//第三步
client.close();
os.close();
dos.close();
}
}
服务端:
package com.Stream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerTest//服务端
{
public static void main(String[] args) throws IOException
{
System.out.println("服务端启动————————");
//创建套接字,指定端口号
ServerSocket ss = new ServerSocket(9999);
//通过ServerSocket的方法accept获取服务端的套接字
Socket server = ss.accept();
InputStream in = server.getInputStream();
DataInputStream dis = new DataInputStream(in);
String str = dis.readUTF();
System.out.println("客户端说: "+ str);
//关闭资源
ss.close();
server.close();
in.close();
dis.close();
}
}
注意⚠️:
- 先运行服务端,再运行客户端
- 如果在客户端创建套接字
Socket client = new Socket("localhost",9999);
,运行时出现异常时,可将IP地址改为localhost。
三.双向通信
上面的例子只是,客户端对服务端说你好,所以称为单向通信。接下来的这个例子,客户端在接受到服务端的问候后,对其回应,称之为双向通信。
客户端:
package com.Stream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientTest//客户端
{
public static void main(String[] args) throws UnknownHostException, IOException {
//第一步
Socket client = new Socket("localhost",9999);
//第二步
OutputStream os = client.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("你好");
//客户端接受服务端的数据(新加部分)
client.shutdownOutput();
InputStream is = client.getInputStream();
DataInputStream dis = new DataInputStream(is);
String str=dis.readUTF();
System.out.println("服务端对我说" + str );
//第三步
client.close();
os.close();
dos.close();
}
}
服务端:
package com.Stream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerTest//服务端
{
public static void main(String[] args) throws IOException
{
System.out.println("服务端启动————————");
//创建套接字,指定端口号
ServerSocket ss = new ServerSocket(9999);
//通过ServerSocket的方法accept获取服务端的套接字
Socket server = ss.accept();
InputStream in = server.getInputStream();
DataInputStream dis = new DataInputStream(in);
String str = dis.readUTF();
System.out.println("客户端说: "+ str);
//回应客户端(新加部分)
server.shutdownInput();
OutputStream ops = server.getOutputStream();
DataOutputStream dos = new DataOutputStream(ops);
dos.writeUTF("我很好!");
//关闭资源
ss.close();
server.close();
in.close();
dis.close();
}
}
四.模拟登陆
上文学习了如何双向通信,下面我们来应用双向通信来模拟登陆账号。
基本思路:登陆账号时,我们输入的账号密码以流的形式发送给服务端,服务端接收到我们的请求后在自己的数据库中寻找是否存在这样的账号密码对,如果有,则登陆成功,没有,则登陆失败。
问题:我们需要填写账号和密码,那我们应该用什么形式来存储用户输入的账号密码?
解决方案:我们可以将账号密码封装成一个对象,然后通过序列化转为文本形式发送给服务端。如果不了解序列化,请参考文章《Java对象的序列化与反序列化》
package com.Stream;
import java.io.Serializable;
public class Account implements Serializable
{
private String username;
private String password;
public Account(String username, String password)
{
super();
this.username = username;
this.password = password;
}
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;
}
}
客户端:
package com.Stream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class ClientTest
{
public static void main(String[] args) throws UnknownHostException, IOException {
Socket client = new Socket("localhost",8888);
//从键盘录入账号密码
Scanner sc =new Scanner(System.in);
System.out.print("Please enter username: ");
String username = sc.next();
System.out.print("Please enter password: ");
String password = sc.next();
//封装成对象
Account account = new Account(username,password);
//传输数据
OutputStream os = client.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os); //使用对象流传输数据
oos.writeObject(account);
//客户端接收数据
client.shutdownOutput();
InputStream in1 = client.getInputStream();
DataInputStream dis1 = new DataInputStream(in1);
System.out.println(dis1.readUTF());
//关闭资源
client.close();
os.close();
dis1.close();
oos.close();
in1.close();
sc.close();
}
}
服务端:
package com.Stream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerTest
{
public static void main(String[] args) throws IOException, ClassNotFoundException
{
ServerSocket ss = new ServerSocket(8888);
Socket server = ss.accept();
InputStream in = server.getInputStream();
ObjectInputStream oos = new ObjectInputStream(in);
Account account = (Account)(oos.readObject()); //将Object类型转化为Account类型
//进行校验
String res = null;
if((account.getUsername().equals("kevin123")&& (account.getPassword()).equals("123456789"))) {
res ="Login successful!";
}else {
res ="The credentials provided are invalid.";
}
//回应客户端
server.shutdownInput();
OutputStream oos2 = server.getOutputStream();
DataOutputStream oos3 = new DataOutputStream(oos2);
oos3.writeUTF(res);
//关闭资源
ss.close();
server.close();
in.close();
oos2.close();
oos3.close();
oos.close();
}
}
登陆成功:
登陆失败:
五.使用try-catch捕获异常
不使用抛出异常的形式处理异常,该用try-catch-finally捕获异常。
在finally里的操作,if(xxx !=null)
可以预防空指针异常
客户端
package com.Stream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientTest
{
public static void main(String[] args) {
Socket client = null;
OutputStream os = null;
ObjectOutputStream oos =null;
InputStream in1 = null;
DataInputStream dis1 =null;
try
{
client = new Socket("localhost",8888);
//从键盘录入账号密码
Scanner sc =new Scanner(System.in);
System.out.print("Please enter username: ");
String username = sc.next();
System.out.print("Please enter password: ");
String password = sc.next();
sc.close();
//封装成对象
Account acc = new Account(username,password);
//传输数据
os = client.getOutputStream();
oos =new ObjectOutputStream(os); //使用对象流传输数据
oos.writeObject(acc);
//客户端接收数据
client.shutdownOutput();
in1=client.getInputStream();
dis1 =new DataInputStream(in1);
String result = dis1.readUTF();
System.out.println(result);
}
catch (IOException e)
{
e.printStackTrace();
}finally {
try
{
if(client != null){
client.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(os != null) {
os.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(dis1 != null) {
dis1.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(oos!= null) {
oos.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(in1 != null){
in1.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
服务端:
package com.Stream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerTest
{
public static void main(String[] args)
{
ServerSocket ss =null;
Socket server =null;
InputStream in = null;
ObjectInputStream oos = null;
OutputStream oos2 = null;
DataOutputStream oos3 = null;
try
{
ss = new ServerSocket(8888);
server = ss.accept();
in = server.getInputStream();
oos=new ObjectInputStream(in);
Account account = (Account)(oos.readObject()); //将Object类型转化为Account类型
//进行校验
String res = null;
if((account.getUsername().equals("kevin123")&& (account.getPassword()).equals("123456789"))) {
res ="Login successful!";
}else {
res ="The credentials provided are invalid.";
}
//回应客户端
server.shutdownInput();
oos2 = server.getOutputStream();
oos3 =new DataOutputStream(oos2);
oos3.writeUTF(res);
}
catch (IOException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
} finally {
//关闭资源
try
{
if(ss != null) {
ss.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
try
{
if(server != null) {
server.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
try
{
if(in != null) {
in.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
try
{
if(oos2 !=null) {
oos2.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
try
{
if(oos3 != null) {
oos3.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
try
{
if(oos !=null) {
oos.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
六.如何使服务器端支持多个客户端同时工作?
上面的例子,如果客户端输入一次账号密码,服务端判断完正确错误服务器就关闭。在实际生活中,一个服务端应该不停的工作,支持多个客户端登陆操作。
一个服务器端一般都需要同时为多个客户端提供通讯,如果需要同时支持多个客户端,则必须使用前面介绍的线程的概念。简单来说,也就是当服务器端接收到一个连接时,启动一个专门的线程处理和该客户端的通讯。
服务端:
package com.Stream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server
{
public static void main(String[] args)
{
ServerSocket ss =null;
Socket server =null;
try
{
ss = new ServerSocket(8888);
while(true) {
server = ss.accept(); //通过accept获取客户端的套接字
new ServerThread(server);//每接收一个就是一个线程来处理
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
服务端线程类:
package com.Stream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ServerThread extends Thread
{
Socket server =null;
InputStream in = null;
ObjectInputStream oos = null;
OutputStream oos2 = null;
DataOutputStream oos3 = null;
public ServerThread(Socket server) //使用构造器让两个方法相互关联
{
super();
this.server = server;
start();
}
@Override
public void run() {
try
{
in = server.getInputStream();
oos=new ObjectInputStream(in);
Account account = (Account)(oos.readObject()); //将Object类型转化为Account类型
//进行校验
String res = null;
if((account.getUsername().equals("kevin123")&& (account.getPassword()).equals("123456789"))) {
res ="Login successful!";
}else {
res ="The credentials provided are invalid.";
}
//回应客户端
server.shutdownInput();
oos2 = server.getOutputStream();
oos3 =new DataOutputStream(oos2);
oos3.writeUTF(res);
}
catch (IOException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
} finally {
try
{
if(in != null) {
in.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(oos2 != null) {
oos2.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(oos3 != null) {
oos3.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(oos != null) {
oos.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
客户端:
package com.Stream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client
{
public static void main(String[] args) {
Socket client = null;
OutputStream os = null;
ObjectOutputStream oos =null;
InputStream in1 = null;
DataInputStream dis1 =null;
try
{
client = new Socket("localhost",8888);
//从键盘录入账号密码
Scanner sc =new Scanner(System.in);
System.out.print("Please enter username: ");
String username = sc.next();
System.out.print("Please enter password: ");
String password = sc.next();
sc.close();
//封装成对象
Account acc = new Account(username,password);
//传输数据
os = client.getOutputStream();
oos =new ObjectOutputStream(os); //使用对象流传输数据
oos.writeObject(acc);
//客户端接收数据
client.shutdownOutput();
in1=client.getInputStream();
dis1 =new DataInputStream(in1);
String result = dis1.readUTF();
System.out.println(result);
}
catch (IOException e)
{
e.printStackTrace();
}finally {
try
{
if(client != null){
client.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(os != null) {
os.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(dis1 != null) {
dis1.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(oos!= null) {
oos.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
try
{
if(in1 != null){
in1.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}