TCP通信的双方需要建立连接,所以先由一方监听某个端口,等待其他设备来连接,这一方称为服务器端。另一方向服务器端发起连接请求,称为客户端。服务器端接受客户端的连接请求后,双方之间的连接建立起来。连接建立后,双方对于连接的使用是相同的,都可以通过连接发送和接收数据。
如果双方通信时没有像HTTP协议这种一问一答的固定模式,就需要随时接收和处理对方发来的数据,所以要把接收和处理数据的工作在一个单独的线程中执行。如果服务器端想同时与多个客户端通信,要对每个客户端的连接建立一个接收线程。
下面通过一个简单的聊天室原型来演示如何编程实现TCP通信。聊天室系统包含一个服务器和多个客户端。服务器对从一个客户端接收的信息,要转发到所有客户端。客户端要把从服务器接收的信息显示给用户,并把用户输入的信息发送到服务器。为实现这一功能,系统的结构要像下面这个图这样设计。
聊天服务器端有一个TCP监听线程,当有客户端发来连接请求时,负责接受客户端的连接请求并创建接收线程。同时,用一个列表记下所有的客户端连接。这样,接收线程就能够将接收到的信息发往列表中保存的所有客户端。客户端也有两个线程,接收线程负责从服务器接收数据并显示给用户,主线程接收用户的输入并发往服务器。
服务器端运行在JavaSE上,的具体代码是这样的:
监听线程ListeningThread首先创建一个ServerSocket对象,然后循环调用它的accept方法等待接受客户端的连接请求。当有客户端发来连接请求时,accept方法返回一个Socket对象。监听线程把Socket对象添加到连接列表connections,并创建一个接收线程ServiceThread。
接收线程循环调用readLine,一旦客户端有消息发来,readLine就会返回发来的消息内容,然后调用chatMessage发送到所有客户端(包括自己)。
chatMessage方法遍历客户端列表connections,把客户端发来的消息转发到所有客户端。
客户端是一个Android应用,界面是这样的:最上面一行文本框中输入服务器的IP地址;下面TCP一行的三个按钮控制建立连接、发送信息、断开连接;再下面UDP一行三个按钮用于测试UDP通信方式;再下面的Content文本框是要发送的聊天消息;最下面的文本是所有聊天内容。
当点击TCP的Connect按钮时,以服务器端的IP地址和端口号为参数创建一个Socket对象,这样就会向服务器发送一个建立连接的请求。如果服务器接受请求,那么连接就成功建立,Socket对象也会创建成功,否则会产生异常,转入异常处理流程。
有了Socket对象后,接着创建一个接收数据的线程。注意这部分代码也需要用AsyncTask异步执行,不能占用主线程。接收线程中循环调用in.readLine方法不断读取服务器发来的消息,一旦有消息发过来,该方法就会返回消息内容。接着再调用sendMessage把消息内容传给主线程显示,因为子线程中不能直接操作界面控件。
传递消息内容用的Android的Handler机制,主线程中创建一个Handler对象,并在它的handleMessage方法中接收消息。子线程中调用该Handler对象的sendMessage方法传递消息。这样就能在线程间传递消息了。
当点击TCP的Send按钮时,代码是把Content文本框中的字符串通过Socket连接发送到服务器。
Android端的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="Server:"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintEnd_toStartOf="@+id/editTextServer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/editTextServer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
app:layout_constraintBaseline_toBaselineOf="@+id/textView1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView1">
<requestFocus
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</EditText>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="TCP: "
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/buttonTCPConnect" />
<Button
android:id="@+id/buttonTCPConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonTCPSend"
app:layout_constraintStart_toEndOf="@+id/textView2" />
<Button
android:id="@+id/buttonTCPSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:text="Send"
android:textAllCaps="false"
app:layout_constraintStart_toStartOf="@+id/buttonUDPSend"
app:layout_constraintTop_toBottomOf="@+id/editTextServer" />
<Button
android:id="@+id/buttonTCPClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Close"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonTCPSend"
app:layout_constraintStart_toEndOf="@+id/buttonTCPSend" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="UDP:"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/buttonUDPListen" />
<Button
android:id="@+id/buttonUDPListen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Listen"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonUDPSend"
app:layout_constraintStart_toEndOf="@+id/textView3" />
<Button
android:id="@+id/buttonUDPSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
android:textAllCaps="false"
app:layout_constraintStart_toEndOf="@+id/buttonUDPListen"
app:layout_constraintTop_toBottomOf="@+id/buttonTCPConnect" />
<Button
android:id="@+id/buttonUDPStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonUDPSend"
app:layout_constraintStart_toEndOf="@+id/buttonUDPSend" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Content:"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintBaseline_toBaselineOf="@+id/editTextContent"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/editTextContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView4"
app:layout_constraintTop_toBottomOf="@+id/buttonUDPListen" />
<TextView
android:id="@+id/textViewResult"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextContent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Android端的完整代码如下:
import androidx.appcompat.app.AppCompatActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class MainActivity extends AppCompatActivity {
EditText etServer;
EditText etContent;
TextView tvResult;
Handler mHandler;
Socket tcpSocket;
TCPReceiveThread tcpReceiveThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etServer = findViewById(R.id.editTextServer);
etContent = findViewById(R.id.editTextContent);
tvResult = findViewById(R.id.textViewResult);
etServer.setText("192.168.1.2");
tcpSocket = null;
mHandler = new Handler(){
@Override
public void handleMessage(Message msg){
switch(msg.what) {
case 1: // receive socket message
String received = (String)msg.obj;
tvResult.append(received+"\r\n");
break;
}
}
};
Button btnTCPConnect = findViewById(R.id.buttonTCPConnect);
btnTCPConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
String serverIP = etServer.getText().toString();
// tcpConnect(serverIP); // 不能在主线程中执行网络操作
new AsyncTask<String, Void, Void>(){
@Override
protected Void doInBackground(String... arg0) {
try {
tcpConnect(arg0[0]);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}.execute(serverIP);
}
});
Button btnTCPSend = findViewById(R.id.buttonTCPSend);
btnTCPSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
new AsyncTask<String, Void, Void>(){
@Override
protected Void doInBackground(String... arg0) {
try {
tcpSend(arg0[0]);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}.execute(etContent.getText().toString());
etContent.setText("");
}
});
Button btnTCPClose = (Button) findViewById(R.id.buttonTCPClose);
btnTCPClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
new AsyncTask<Void, Void, Void>(){
@Override
protected Void doInBackground(Void... arg0) {
try {
tcpDisconnect();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
});
}
void tcpConnect(String ip) throws IOException {
if(tcpSocket!=null) tcpDisconnect();
tcpSocket = new Socket(ip, 8899);
tcpSocket.setSoTimeout(1000);
tcpReceiveThread = new TCPReceiveThread();
tcpReceiveThread.start();
}
void tcpSend(String ctx) throws IOException {
ctx += "\r\n";
byte[] buf = ctx.getBytes("UTF-8");
tcpSocket.getOutputStream().write(buf);
}
void tcpDisconnect() throws IOException {
if(tcpReceiveThread!=null)
tcpReceiveThread.setStopFlag(); // 具体的关闭操作在接收线程结束后执行,
}
void sendMessage(String str){
Message msg = Message.obtain(); // 从Message池中取Message对象,用new创建会用到内存分配,影响效率
msg.what = 1;
msg.obj = str;
mHandler.sendMessage(msg);
}
class TCPReceiveThread extends Thread {
private boolean flag;
public void setStopFlag(){
flag = false;
}
@Override
public void run(){
try {
flag = true;
BufferedReader in = new BufferedReader(new InputStreamReader(tcpSocket.getInputStream(), "UTF-8"));
sendMessage("TCP socket connection connected");
String line = null;
while(flag) {
try {
line = in.readLine();
} catch (SocketTimeoutException e){
//e.printStackTrace();
//flag = false;
}
if(line != null) {
System.out.println(line);
sendMessage(line);
line = null;
}
}
in.close();
tcpSocket.close();
tcpSocket = null;
sendMessage("TCP socket connection closed");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Server端在JavaSE平台上实现,完整代码如下:
import java.io.*;
import java.net.*;
import java.util.*;
public class ChatServer {
public static void main(String[] args){
Thread listeningThread = new ListeningThread(8899);
listeningThread.start();
}
}
class ListeningThread extends Thread {
private int port;
private boolean flag = true;
private ServerSocket lServerSocket;
private List<PrintWriter> connections = new Vector<PrintWriter>(); //保存所有连结
public ListeningThread(int aPort){
port = aPort;
}
public void run(){
try {
lServerSocket = new ServerSocket(port);
lServerSocket.setSoTimeout(1000);
System.out.println("TCP Socket Start listening......");
while(flag) {
try {
Socket incoming = lServerSocket.accept();
System.out.println("Accept "+incoming.getInetAddress()+"("+incoming.getPort()+")");
//PrintWriter out = new PrintWriter(incoming.getOutputStream(),true);
PrintWriter out = new PrintWriter(new OutputStreamWriter(incoming.getOutputStream(), "UTF-8"),true);
connections.add(out); //有新连结则将其输出流添加到连结列表中
out.println("Welcome to "+lServerSocket.getInetAddress()+"("+lServerSocket.getLocalPort()+")");
out.flush();
Thread t = new ServiceThread(incoming); //启动接收数据线程
t.start();
}
catch(SocketTimeoutException e) {
if(!flag)break;
}
}
lServerSocket.close();
System.exit(0);
}
catch(IOException e) {
System.out.println(e);
}
}
public synchronized void chatMessage(String msg) {
Iterator<PrintWriter> iter = connections.iterator();
while(iter.hasNext()) {
try {
PrintWriter out = iter.next();
out.println(msg);
out.flush();
}
catch(Exception e) {
iter.remove(); //如果发送中出现异常,则将连结移除
}
}
}
class ServiceThread extends Thread {
private Socket lSocket;
public ServiceThread(Socket aSocket){
lSocket = aSocket;
}
public void run(){
try {
BufferedReader in = new BufferedReader(new InputStreamReader(lSocket.getInputStream(), "UTF-8"));
String line;
while(flag){
line = in.readLine();
if(line != null){
System.out.println(line); // 本地屏幕显示
chatMessage("Chat:"+line); // 发送到所有客户端
}
}
lSocket.close();
System.out.println("Thread stoped.");
}
catch(IOException e){
System.out.println(e);
}
}
}
}
————————————————
转载于:https://blog.csdn.net/nanoage/article/details/127967224