Socket 编程

笔记 · 6 天前 · 23 人浏览
Socket 编程

  Socket 编程是网络通信中的一项基础而强大的技术,它允许两台计算机(或同一台计算机上的两个进程)之间进行双向通信。Socket 可以看作是网络通信中的一个端点,通过它可以发送或接收数据。在 TCP/IP 协议族中, Socket 扮演着核心角色,用于实现不同主机之间的进程间通信。

  简单来说就是一种编程接口,允许在不同主机上的进程(运行中的程序)通过网络进行数据交换。

Socket 的类型

Socket 主要分为两种类型:

流式 Socket(SOCK_STREAM):基于 TCP 协议,提供面向连接的、可靠的数据传输服务。
  数据传输的顺序与发送的顺序相同,数据不重复,且接收方必须读取了发送方发送的所有数据后才能继续接收后续数据。

数据报 Socket(SOCK_DGRAM):基于 UDP 协议,提供无连接的、不可靠的数据传输服务。
  数据报是独立的,保留数据边界,发送方和接收方之间没有明确的连接,且发送方和接收方都可能不按顺序接收数据。

套接字的组成

一个套接字主要由以下三个属性组成:

  1. 网络地址:通常是 IP 地址,用于标识网络上的设备。
  2. 端口号:用于标识设备上的特定应用或进程。端口号是一个16位的数字,范围从0到65535。
  3. 协议:如 TCP(传输控制协议)或 UDP(用户数据报协议),定义了数据传输的规则和格式。

Socket 编程的基本步骤

  Socket 编程的基本步骤大致相同,无论使用的是哪种 Socket 类型(TCP或UDP),但具体细节(如连接管理)会有所不同。以下是一个简化的 TCP Socket 编程步骤:

服务器端

  1. 创建Socket:使用 socket() 函数创建一个新的 Socket。
  2. 绑定Socket:使用 bind() 函数将Socket与特定的 IP 地址和端口号绑定。
  3. 监听连接:使用 listen() 函数使Socket进入被动监听状态,等待客户端的连接请求。
  4. 接受连接:使用 accept()函数接受客户端的连接请求,该函数会返回一个新的 Socket,用于与客户端进行通信。
  5. 数据收发:使用 read() / write()recv() / send() 函数与客户端进行数据的发送和接收。
  6. 关闭连接:数据传输完成后,使用 close() 函数关闭Socket连接。

客户端

  1. 创建Socket:使用 socket() 函数创建一个新的 Socket。
  2. 连接服务器:使用 connect() 函数与服务器建立连接。
  3. 数据收发:使用 read() / write()recv() / send() 函数与服务器进行数据的发送和接收。
  4. 关闭连接:数据传输完成后,使用 close() 函数关闭 Socket 连接。

注意事项

  • 在进行 Socket 编程时,需要注意处理网络异常和错误,例如连接失败、数据接收不完整等。
  • 对于 TCP 连接,要注意处理好连接的建立和断开,避免资源泄露。
  • 对于 UDP 连接,虽然不需要建立连接,但也要注意数据包的完整性和顺序性。
  • 在进行跨网络通信时,需要注意防火墙和网络策略的设置,确保数据能够顺利传输。

示例程序

为方便了解连接建立过程,该仅支持同时建立单个 TCP 连接,实现双方可以互相读写。对于相关函数参数并未做详细解释,如需请自行查看相关文档。目前还是很多都在采用 AF_INET(IPv4),使用时需要注意的是网络字节序和主机字节序转化。

server.c

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

#define PORT 6666
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#define handle_error(cmd, result) \
        if(result < 0)        \
        {                     \
            perror(cmd);      \
            return -1;        \
        }

void * read_from_client(void * arg)
{
    // 使用recv接收客户端发送的数据 打印到控制台
    char * read_buf = NULL;
    int client_fd = *(int *)arg;  // 得到文件描述符

    read_buf = malloc(TYPESIZE);

    ssize_t count = 0;

    if (!read_buf)
    {
        perror("malloc_server_read_buf");
        return NULL;
    }

    // 接收数据
    // 只要能接受到数据 正常使用 一直挂起
    while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
    {
        if (count < 0)
        {
            perror("recv");
        }

        fputs(read_buf, stdout);
    }

    printf("客户端请求关闭\n");
    free(read_buf);

    return NULL;
}

void * write_to_client(void * arg)
{
    // 接收控制台输入的信息 写出去
    char * write_buf = NULL;
    int client_fd = *(int *)arg;  // 得到文件描述符

    write_buf = malloc(TYPESIZE);

    ssize_t count = 0;

    if (!write_buf)
    {
        perror("malloc_server_write_buf");
        return NULL;
    }

    while (fgets(write_buf, BUFFSIZE, stdin) != NULL)
    {
        // 发送数据
        count = send(client_fd, write_buf, BUFFSIZE, 0);
        if (count < 0)
        {
            perror("send");
        }
    }

    printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n");
    // 可以具体到关闭某一端
    shutdown(client_fd, SHUT_WR);
    free(write_buf);

    return NULL;
}

int main(int argc, char const *argv[])
{
    struct sockaddr_in server_addr, client_addr;
    int sock_fd, tmp_res, client_fd;

    // 清空
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 填写服务端类型
    server_addr.sin_family = AF_INET;  // ipv4
    // 填写IP地址  0.0.0.0
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr);
    // 填写端口号
    server_addr.sin_port = htons(PORT);

    /******** 网络编程 socket 流程 ********/
    // 1. 创建socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    handle_error("socket", sock_fd);

    // 2. 绑定地址
    tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    handle_error("bind", tmp_res);

    // 3. 进入监听状态
    tmp_res = listen(sock_fd, 128);  // 等待队列长度,可以随便填
    handle_error("listen", tmp_res);

    // 4. 获取客户端的连接
    socklen_t client_addr_len = sizeof(client_addr);
    // 返回的文件描述符才是能够和客户端收发消息的文件描述符
    // 如果调用accept之后没有客户端连接,这里会挂起等待
    client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &client_addr_len);
    handle_error("accept", client_fd);

    printf("与客户端%s:%d建立连接 --> 文件描述符是 %d\n", 
            inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);

    // 创建子线程用于收消息
    pthread_t pid_read, pid_write;
    pthread_create(&pid_read, NULL, read_from_client, (void *)&client_fd);
    // 创建子线程用于发消息
    pthread_create(&pid_write, NULL, write_to_client, (void *)&client_fd);

    // 阻塞主线程
    pthread_join(pid_read, NULL);
    pthread_join(pid_write, NULL);

    // 释放资源
    printf("释放资源\n");
    close(client_fd);
    close(sock_fd);

    return 0;
}

client.c

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

#define ADDRESS_S ("127.0.0.1")
#define ADDRESS_C ("192.168.100.137")
#define PORT_S 6666
#define PORT_C 8888
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#define handle_error(cmd, result) \
        if(result < 0)        \
        {                     \
            perror(cmd);      \
            return -1;        \
        }

void * read_from_server(void * arg)
{
    // 使用recv接收客户端发送的数据 打印到控制台
    char * read_buf = NULL;
    int client_fd = *(int *)arg;  // 得到文件描述符

    read_buf = malloc(TYPESIZE);

    ssize_t count = 0;

    if (!read_buf)
    {
        perror("malloc_server_read_buf");
        return NULL;
    }

    // 接收数据
    // 只要能接受到数据 正常使用 一直挂起
    while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
    {
        if (count < 0)
        {
            perror("recv");
        }

        fputs(read_buf, stdout);
    }

    printf("服务端请求关闭\n");
    free(read_buf);

    return NULL;
}

void * write_to_server (void * arg)
{
    // 接收控制台输入的信息 写出去
    char * write_buf = NULL;
    int client_fd = *(int *)arg;  // 得到文件描述符

    write_buf = malloc(TYPESIZE);

    ssize_t count = 0;

    if (!write_buf)
    {
        perror("malloc_server_write_buf");
        return NULL;
    }

    while (fgets(write_buf, BUFFSIZE, stdin) != NULL)
    {
        // 发送数据
        count = send(client_fd, write_buf, BUFFSIZE, 0);
        if (count < 0)
        {
            perror("send");
        }
    }

    printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n");
    // 可以具体到关闭某一端
    shutdown(client_fd, SHUT_WR);
    free(write_buf);

    return NULL;
}

int main(int argc, char const *argv[])
{
    struct sockaddr_in server_addr, client_addr;
    int sock_fd, tmp_res;

    // 清空
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 填写客户端类型
    client_addr.sin_family = AF_INET;  // ipv4
    // 填写IP地址  0.0.0.0
    inet_pton(AF_INET, ADDRESS_C, &client_addr.sin_addr);
    // 填写端口号
    client_addr.sin_port = htons(PORT_C);

    // 填写服务端类型
    server_addr.sin_family = AF_INET;  // ipv4
    // 填写IP地址  
    inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr);
    // 填写端口号
    server_addr.sin_port = htons(PORT_S);

    /******** 网络编程 socket 流程 ********/
    // 1. 创建socket,只有一个文件描述符
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    handle_error("socket", sock_fd);

    // 2. 绑定
    tmp_res = bind(sock_fd, (struct sockaddr *)&client_addr, sizeof(client_addr));
    handle_error("bind", tmp_res);

    // 3. 客户端主动连接服务端
    tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    handle_error("connect", tmp_res);

    printf("连接上服务端_%s:%d\n", 
        inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

    // 创建子线程用于收消息
    pthread_t pid_read, pid_write;
    pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd);
    // 创建子线程用于发消息
    pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd);

    // 阻塞主线程
    pthread_join(pid_read, NULL);
    pthread_join(pid_write, NULL);

    // 释放资源
    printf("释放资源\n");
    close(sock_fd);

    return 0;
}

  在上述例程中,将客户端绑定到了 192.168.100.137 的 8888 端口,192.168.100.137 实际上是本机 IP,此处等价于 localhost 或 127.0.0.1。此外,通常服务端不需要绑定到具体的 IP 和端口,如果不绑定,启动后会操作系统会随机为客户端分配本机的某个端口。这里将客户端绑定至指定的 IP 和端口,主要是为了在分析时便于区分客户端和服务端,实际的客户端程序完全可以省去这一步。

C Socket
Theme Jasmine by Kent Liao

本网站由 又拍云 提供CDN加速/云存储服务

鄂ICP备2023005457号    鄂公网安备 42011302000815号