Skip to content

Socket 网络通信基础

什么是 Socket?

Socket(套接字)是网络编程中最基本的概念。它可以看作是应用程序与网络之间的一个接口,通过 Socket 可以实现不同计算机之间的数据交换。

Socket 的比喻

可以把 Socket 比作是打电话的过程:

  • IP 地址 - 电话号码
  • 端口 - 分机号
  • Socket - 电话机本身
  • 发送数据 - 拨号和说话
  • 接收数据 - 接听和听话

Socket 的分类

根据通信协议,Socket 分为两种主要类型:

  1. TCP Socket(面向连接)

    • 可靠的连接
    • 数据按顺序到达
    • 适合需要可靠性的应用(如文件传输、远程登录)
    • 建立连接需要三次握手
  2. UDP Socket(无连接)

    • 无连接,直接发送数据包
    • 数据可能丢失或乱序
    • 低延迟,高效率
    • 适合实时通信(如视频直播、在线游戏)

LeBot 中的 Socket 应用

在 LeBot 中,Socket 用于:

  • 与远程控制端进行实时通信
  • 与多个传感器节点交换数据
  • 实现分布式系统的节点间通信
  • 上传和下载数据

TCP/IP 协议栈

为了理解 Socket 编程,需要了解 TCP/IP 协议栈的基本结构。

协议层次

应用层     - HTTP, FTP, SMTP, SSH, ROS, 自定义协议
┌────────────────────────────────────┐
│ 传输层    - TCP, UDP
├────────────────────────────────────┤
│ 网络层    - IP(IPv4, IPv6)
├────────────────────────────────────┤
│ 链路层    - 以太网, WiFi
└────────────────────────────────────┘
硬件 - 网卡、路由器等

重要的网络概念

  • IP 地址:唯一标识网络上的一台计算机

    • IPv4:192.168.1.100(32 位)
    • IPv6:2001:db8::1(128 位)
  • 端口:运行在同一计算机上的程序之间的区分

    • 范围:0-65535
    • 0-1023:保留端口(需要 root 权限)
    • 1024-49151:注册端口
    • 49152-65535:动态端口
  • 协议:定义通信规则

    • TCP:可靠、有序
    • UDP:快速、可能丢失

TCP 通信原理

TCP 连接建立(三次握手)

建立 TCP 连接需要三次握手:

客户端                              服务器
  |                                  |
  |------------ SYN (seq=x) -------->|  1. 客户端发送 SYN 包
  |                                  |
  |<------- SYN+ACK (seq=y, ack=x+1) |  2. 服务器响应 SYN+ACK
  |                                  |
  |------- ACK (seq=x+1, ack=y+1) -->|  3. 客户端发送 ACK
  |                                  |
  |========== 连接建立 =============|

TCP 连接断开(四次挥手)

关闭连接需要四次挥手:

客户端                              服务器
  |                                  |
  |------------ FIN (seq=x) -------->|  1. 客户端发送 FIN
  |                                  |
  |<---------- ACK (ack=x+1) --------|  2. 服务器确认
  |                                  |
  |<---------- FIN (seq=y) ---------|  3. 服务器发送 FIN
  |                                  |
  |----------- ACK (ack=y+1) ------->|  4. 客户端确认
  |                                  |

UDP 通信原理

与 TCP 不同,UDP 是无连接的协议,直接发送数据包:

发送端                                接收端
  |                                     |
  |-------- 数据包 (目标 IP:端口) ----->|
  |                                     |
  |-------- 数据包 (目标 IP:端口) ----->|  (可能延迟或丢失)
  |                                     |
  |-------- 数据包 (目标 IP:端口) ----->|

Python Socket 编程

TCP 服务器

Python 提供了 socket 模块来进行网络编程。以下是一个简单的 TCP 服务器示例:

python
#!/usr/bin/env python3

import socket
import threading

def handle_client(conn, addr):
    """处理客户端连接"""
    print(f"客户端连接: {addr}")
    
    try:
        while True:
            # 接收数据(最多 1024 字节)
            data = conn.recv(1024)
            
            if not data:
                print(f"客户端 {addr} 断开连接")
                break
            
            # 打印接收到的数据
            message = data.decode('utf-8')
            print(f"从 {addr} 收到: {message}")
            
            # 发送响应
            response = f"服务器已收到: {message}"
            conn.send(response.encode('utf-8'))
            
    finally:
        conn.close()

def start_server(host='0.0.0.0', port=5000):
    """启动 TCP 服务器"""
    # 创建 Socket 对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 解决 "Address already in use" 错误
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 绑定地址和端口
    server_socket.bind((host, port))
    
    # 监听连接(最多 5 个等待连接)
    server_socket.listen(5)
    
    print(f"服务器启动,监听 {host}:{port}")
    
    try:
        while True:
            # 接受客户端连接
            conn, addr = server_socket.accept()
            
            # 为每个客户端创建新线程
            client_thread = threading.Thread(
                target=handle_client, 
                args=(conn, addr)
            )
            client_thread.daemon = True
            client_thread.start()
            
    except KeyboardInterrupt:
        print("\n服务器关闭")
    finally:
        server_socket.close()

if __name__ == '__main__':
    start_server()

TCP 客户端

python
#!/usr/bin/env python3

import socket
import sys

def connect_to_server(host='127.0.0.1', port=5000):
    """连接到 TCP 服务器"""
    
    # 创建 Socket 对象
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        # 连接到服务器
        print(f"正在连接到 {host}:{port}")
        client_socket.connect((host, port))
        print("已连接到服务器")
        
        # 交互式通信
        while True:
            # 获取用户输入
            message = input("输入要发送的消息 (输入 'exit' 退出): ")
            
            if message.lower() == 'exit':
                break
            
            # 发送数据
            client_socket.send(message.encode('utf-8'))
            
            # 接收响应
            response = client_socket.recv(1024)
            print(f"服务器回复: {response.decode('utf-8')}")
            
    except ConnectionRefusedError:
        print(f"错误:无法连接到 {host}:{port}")
    except Exception as e:
        print(f"错误: {e}")
    finally:
        client_socket.close()
        print("已断开连接")

if __name__ == '__main__':
    host = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1'
    port = int(sys.argv[2]) if len(sys.argv) > 2 else 5000
    connect_to_server(host, port)

UDP 服务器

python
#!/usr/bin/env python3

import socket

def start_udp_server(host='0.0.0.0', port=5001):
    """启动 UDP 服务器"""
    
    # 创建 UDP Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    # 绑定地址和端口
    server_socket.bind((host, port))
    
    print(f"UDP 服务器启动,监听 {host}:{port}")
    
    try:
        while True:
            # 接收数据(缓冲区大小 1024 字节)
            data, addr = server_socket.recvfrom(1024)
            
            message = data.decode('utf-8')
            print(f"从 {addr} 收到: {message}")
            
            # 发送响应
            response = f"UDP 服务器已收到: {message}"
            server_socket.sendto(response.encode('utf-8'), addr)
            
    except KeyboardInterrupt:
        print("\n服务器关闭")
    finally:
        server_socket.close()

if __name__ == '__main__':
    start_udp_server()

UDP 客户端

python
#!/usr/bin/env python3

import socket

def send_udp_message(host='127.0.0.1', port=5001, message="Hello UDP"):
    """发送 UDP 消息"""
    
    # 创建 UDP Socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    try:
        # 发送数据(不需要建立连接)
        print(f"发送到 {host}:{port}: {message}")
        client_socket.sendto(message.encode('utf-8'), (host, port))
        
        # 接收响应
        response, _ = client_socket.recvfrom(1024)
        print(f"服务器回复: {response.decode('utf-8')}")
        
    except Exception as e:
        print(f"错误: {e}")
    finally:
        client_socket.close()

if __name__ == '__main__':
    send_udp_message()

C/C++ Socket 编程

TCP 服务器(C 语言)

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT 5000
#define BUFFER_SIZE 1024

void* handle_client(void* arg) {
    int client_fd = *(int*)arg;
    char buffer[BUFFER_SIZE];
    
    printf("客户端连接\n");
    
    while (1) {
        // 接收数据
        int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
        
        if (n <= 0) {
            printf("客户端断开连接\n");
            break;
        }
        
        buffer[n] = '\0';
        printf("收到消息: %s\n", buffer);
        
        // 发送响应
        char response[BUFFER_SIZE];
        snprintf(response, BUFFER_SIZE, "服务器已收到: %s", buffer);
        send(client_fd, response, strlen(response), 0);
    }
    
    close(client_fd);
    free(arg);
    pthread_exit(NULL);
}

int main() {
    int server_fd;
    struct sockaddr_in server_addr;
    
    // 创建 Socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }
    
    // 设置 Socket 选项
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    // 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);
    
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        return 1;
    }
    
    // 监听连接
    listen(server_fd, 5);
    printf("服务器启动,监听端口 %d\n", PORT);
    
    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        
        // 接受客户端连接
        int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }
        
        // 创建线程处理客户端
        pthread_t thread_id;
        int* arg = malloc(sizeof(int));
        *arg = client_fd;
        pthread_create(&thread_id, NULL, handle_client, arg);
        pthread_detach(thread_id);
    }
    
    close(server_fd);
    return 0;
}

TCP 客户端(C 语言)

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BUFFER_SIZE 1024

int main(int argc, char* argv[]) {
    const char* host = argc > 1 ? argv[1] : "127.0.0.1";
    int port = argc > 2 ? atoi(argv[2]) : 5000;
    
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    
    // 创建 Socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        return 1;
    }
    
    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, host, &server_addr.sin_addr);
    
    // 连接到服务器
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        return 1;
    }
    
    printf("已连接到服务器 %s:%d\n", host, port);
    
    // 交互式通信
    while (1) {
        printf("输入消息 (输入 'exit' 退出): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        // 移除换行符
        buffer[strcspn(buffer, "\n")] = 0;
        
        if (strcmp(buffer, "exit") == 0) {
            break;
        }
        
        // 发送数据
        send(sock_fd, buffer, strlen(buffer), 0);
        
        // 接收响应
        int n = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
        if (n > 0) {
            buffer[n] = '\0';
            printf("服务器回复: %s\n", buffer);
        }
    }
    
    close(sock_fd);
    return 0;
}

Socket 编程的关键概念

阻塞 vs 非阻塞

阻塞 Socket(默认)

  • 在等待数据时会一直阻塞
  • 简单易用,但可能降低效率

非阻塞 Socket

  • 立即返回,不管是否有数据
  • 效率高,但编程复杂
python
import socket

# 设置为非阻塞
sock = socket.socket()
sock.setblocking(False)

# 或使用 settimeout
sock.settimeout(1.0)  # 1 秒超时

缓冲区管理

接收数据时需要考虑缓冲区大小:

python
# 一次接收 1024 字节
data = sock.recv(1024)

# 一次接收全部数据(需要知道数据长度)
def recv_all(sock, length):
    data = b''
    while len(data) < length:
        chunk = sock.recv(min(1024, length - len(data)))
        if not chunk:
            break
        data += chunk
    return data

错误处理

Socket 编程中的错误处理至关重要:

python
import socket
import errno

try:
    sock.connect((host, port))
except socket.timeout:
    print("连接超时")
except ConnectionRefusedError:
    print("连接被拒绝")
except OSError as e:
    if e.errno == errno.ECONNRESET:
        print("连接被重置")
    else:
        print(f"Socket 错误: {e}")

LeBot 中的 Socket 应用示例

实时传感器数据接收

python
#!/usr/bin/env python3

import socket
import json
import threading
import time

class SensorDataReceiver:
    def __init__(self, port=5002):
        self.port = port
        self.running = False
        self.sensor_data = {}
        
    def start_server(self):
        """启动传感器数据接收服务器"""
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind(('0.0.0.0', self.port))
        server_socket.listen(5)
        
        print(f"传感器数据服务器启动,监听端口 {self.port}")
        self.running = True
        
        try:
            while self.running:
                conn, addr = server_socket.accept()
                thread = threading.Thread(
                    target=self.handle_sensor_data,
                    args=(conn, addr)
                )
                thread.daemon = True
                thread.start()
        except KeyboardInterrupt:
            print("服务器关闭")
        finally:
            server_socket.close()
    
    def handle_sensor_data(self, conn, addr):
        """处理传感器数据"""
        print(f"传感器节点连接: {addr}")
        
        while self.running:
            try:
                data = conn.recv(1024)
                if not data:
                    break
                
                # 解析 JSON 格式的传感器数据
                sensor_info = json.loads(data.decode('utf-8'))
                sensor_id = sensor_info.get('id')
                
                self.sensor_data[sensor_id] = sensor_info
                print(f"传感器 {sensor_id}: {sensor_info}")
                
                # 发送确认
                ack = json.dumps({'status': 'ok'})
                conn.send(ack.encode('utf-8'))
                
            except json.JSONDecodeError:
                print(f"无法解析数据: {data}")
            except Exception as e:
                print(f"错误: {e}")
                break
        
        conn.close()
    
    def get_sensor_data(self, sensor_id):
        """获取特定传感器的数据"""
        return self.sensor_data.get(sensor_id)

if __name__ == '__main__':
    receiver = SensorDataReceiver()
    receiver.start_server()

远程控制命令发送

python
#!/usr/bin/env python3

import socket
import json

class RemoteController:
    def __init__(self, host='192.168.1.100', port=5003):
        self.host = host
        self.port = port
        self.sock = None
    
    def connect(self):
        """连接到机器人"""
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.sock.connect((self.host, self.port))
            print(f"已连接到机器人 {self.host}:{self.port}")
            return True
        except Exception as e:
            print(f"连接失败: {e}")
            return False
    
    def send_command(self, command, **kwargs):
        """发送控制命令"""
        if not self.sock:
            print("未连接到机器人")
            return False
        
        msg = {
            'command': command,
            'params': kwargs
        }
        
        try:
            self.sock.send(json.dumps(msg).encode('utf-8'))
            
            # 等待响应
            response = self.sock.recv(1024)
            result = json.loads(response.decode('utf-8'))
            
            return result.get('status') == 'success'
        except Exception as e:
            print(f"发送命令失败: {e}")
            return False
    
    def move_forward(self, speed=0.5, duration=1.0):
        """前进命令"""
        return self.send_command('move', direction='forward', speed=speed, duration=duration)
    
    def rotate(self, angle=90, speed=1.0):
        """旋转命令"""
        return self.send_command('rotate', angle=angle, speed=speed)
    
    def stop(self):
        """停止命令"""
        return self.send_command('stop')
    
    def close(self):
        """断开连接"""
        if self.sock:
            self.sock.close()
            print("已断开连接")

if __name__ == '__main__':
    controller = RemoteController()
    
    if controller.connect():
        # 发送一系列控制命令
        controller.move_forward(speed=0.5, duration=2.0)
        time.sleep(2)
        
        controller.rotate(angle=90, speed=1.0)
        time.sleep(1)
        
        controller.stop()
        controller.close()

Socket 编程的最佳实践

  1. 始终检查返回值 - 确保每个 Socket 操作都成功
  2. 适当的错误处理 - 捕获并处理所有可能的异常
  3. 资源清理 - 使用 try-finally 确保 Socket 正确关闭
  4. 缓冲区管理 - 处理粘包和缓冲区溢出问题
  5. 心跳包 - 定期发送心跳包保持连接
  6. 超时设置 - 防止长时间挂起
  7. 多线程同步 - 正确处理并发访问

总结

Socket 是网络编程的基础,理解 TCP/UDP 的工作原理和 Socket 编程的基本方法对于构建分布式机器人系统至关重要。本章介绍了:

  • Socket 的基本概念和分类
  • TCP 和 UDP 的工作原理
  • Python 和 C 中的 Socket 编程
  • LeBot 中的实际应用

掌握 Socket 编程后,你可以构建高效、可靠的机器人通信系统。在接下来的章节中,我们将探讨蓝牙通信,这是 LeBot 与移动设备交互的重要方式。


推荐资源

由 LeBot 开发团队编写