35、使用套接字进行网络编程

使用套接字进行网络编程

1. 获取本地主机名

在网络编程中,有时需要获取本地主机的名称。可以使用 gethostname 函数来实现这一目的:

int gethostname(char *name, int len);

该函数将本地主机名放入 name 指向的字符数组中,数组大小为 len 字节。成功时返回 0,失败时返回 -1,并将错误原因存储在 errno 中。需要注意的是,根据主机的具体配置, gethostname 可能返回也可能不返回主机的完全限定域名。

2. 主机地址

主机名便于人类识别主机,但对于网络软件来说,仅靠主机名无法充分利用。因此,每个主机还有一个唯一的 32 位主机地址,网络上的每个主机地址都不同。

主机地址通常用“点分十进制”表示,例如十六进制地址 0x7b2d4359 会写成 123.45.67.89 。每个网络地址由两部分组成:网络号和主机号。地址分为不同类型:
| 地址类型 | 网络号字节数 | 主机号字节数 |
| ---- | ---- | ---- |
| Class A | 1 | 3 |
| Class B | 2 | 2 |
| Class C | 3 | 1 |

还可以进一步划分主机号部分,一部分表示子网号,其余部分表示该子网上的主机号。网络号用于网络路由软件决定如何将数据从一个网络传输到另一个网络,子网号指示在给定网络内将数据传输到网络的哪一部分,主机号则指定接收数据的具体主机。

为了在主机名和主机地址之间进行转换,提供了以下函数:

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>

struct hostent *gethostent(void);
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const char *addr, int len, int type);
int sethostent(int stayopen);
int endhostent(void);

这些函数会根据系统配置在不同数据库中查找主机名和主机地址。 /etc/hosts 文件列出了主机名和地址对,通常仅用于局域网地址。网络信息服务(黄页)为 /etc/hosts 文件提供了不同的接口,名称服务器则提供了按域分布的主机名和地址信息数据库。在 SVR4 中, /etc/nsswitch.conf 文件控制使用哪些数据库以及搜索顺序。

下面是这些函数的简要说明:
- sethostent :打开数据库并将“当前条目”指针设置到文件开头。 stayopen 参数非零表示在调用其他函数时数据库保持打开,减少打开数据库的系统调用次数。
- endhostent :关闭数据库。
- gethostent :从数据库中读取下一个主机名和地址并返回。
- gethostbyname :在数据库中搜索指定名称的主机条目并返回。
- gethostbyaddr :在数据库中搜索指定地址的主机条目并返回。

如果未找到条目或遇到文件结束符,这些函数都返回 NULL 。成功时,它们返回一个指向 struct hostent 结构的指针:

struct hostent {
    char     *h_name;
    char    **h_aliases;
    int       h_addrtype;
    int       h_length;
    char     *h_addr_list;
};

各字段含义如下:
- h_name :主机的官方名称(通常是完全限定域名)。
- h_aliases :主机的其他名称指针。
- h_addrtype :地址类型。
- h_length :地址长度(字节)。
- h_addr_list :主机的地址列表。

老系统使用 h_addr 字段而不是 h_addr_list ,新系统为了向后兼容,通常将 h_addr 定义为 h_addr_list[0]

3. 服务和端口号

网络上的每个主机可以提供多种网络服务,如远程登录、文件传输、电子邮件传递等。为了区分不同服务的数据,每个服务被分配一个端口号,端口号是一个小整数,用于标识数据要交付的服务。

两个主机使用某种服务进行通信时,必须就该服务使用的端口号达成一致。所有标准 Internet 协议都使用知名端口,例如使用文件传输协议(FTP)时,主机应使用端口号 21。

大多数 UNIX 版本(包括 SVR4)使用 /etc/services 文件存储知名端口号列表,该文件列出了服务名称、端口号和协议(TCP 或 UDP)。可以使用以下函数读取该文件:

#include <netdb.h>

struct servent *getservent(void);
struct servent *getservbyname(const char *name, char *proto);
struct servent *getservbyport(int port, char *proto);
int setservent(int stayopen);
int endservent(void);

这些函数的功能如下:
- setservent :打开服务文件并将“当前条目”指针设置到文件开头。 stayopen 参数非零表示文件在调用其他函数时保持打开。
- endservent :关闭服务文件。
- getservent :读取文件中的下一个条目并返回。
- getservbyname :搜索指定名称的服务条目并返回。
- getservbyport :搜索指定端口号的服务条目并返回。

proto 参数可以是“tcp”或“udp”,因为实际上有两组端口号,分别用于基于流的 TCP 服务和基于数据报的 UDP 服务。如果未找到条目或遇到文件结束符,这些函数返回 NULL 。成功时,它们返回一个指向 struct servent 结构的指针:

struct servent {
    char     *s_name;
    char    **s_aliases;
    int       s_port;
    char     *s_proto;
};

各字段含义如下:
- s_name :服务的官方名称。
- s_aliases :服务的其他名称。
- s_port :端口号。
- s_proto :与服务通信时使用的协议。

4. 网络字节序

计算机制造商在实现整数存储时有两种选择:大端字节序(将最高有效字节放在最低内存地址)和小端字节序(将最高有效字节放在最高内存地址)。例如,Intel 芯片(80x86)和 Digital Equipment Corp. VAX 计算机是小端字节序架构,Motorola 680x0 芯片和 Sun SPARC 系统是大端字节序架构。

由于不同机器可能使用不同的字节序,为了确保网络数据的正确传输,定义了网络字节序(大端字节序)。所有从网络到达主机的流量都将采用相同的格式,主机再将其转换为内部使用的格式。同样,主机发送的所有流量在离开前都要转换为网络字节序。

Berkeley 网络编程范例规定每个网络程序必须自行执行字节序转换,为此提供了以下四个函数:

#include <sys/types.h>
#include <netinet/in.h>

u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
u_long ntohl(u_long netlong);
u_long ntohs(u_short netshort);
  • htonl :将 32 位主机长整型值从主机字节序转换为网络字节序。
  • htons :将 16 位主机短整型值从主机字节序转换为网络字节序。
  • ntohl :将 32 位网络长整型值从网络字节序转换为主机字节序。
  • ntohs :将 16 位网络短整型值从网络字节序转换为主机字节序。

这些函数通常作为 C 预处理器宏实现,根据主机架构可能是“无操作”。在网络上交换整数数据时,务必使用这些函数。字符字符串不需要转换,因为它们是单字节值的数组。没有网络浮点格式,浮点数字通常应转换为整数或打印为字符串后再进行交换。

5. 创建套接字

Berkeley 网络编程范例中的基本通信单元是套接字,可以使用 socket 函数创建:

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明如下:
- domain :指定地址解释的域或地址族,对地址长度和含义有一定限制。常见的有 AF_UNIX (地址是普通 UNIX 路径名)和 AF_INET (用于 Internet 地址)。
- type :指定支持的通信通道类型,有两种选择:
- SOCK_STREAM :通常称为虚拟电路,是双向连续字节流,保证数据按发送顺序可靠传输。在电路建立之前不能发送数据,直到通信完成电路才会断开。例如电话通话就是虚拟电路的现实例子,FIFO 也是。在 Internet 域中,使用 Internet 标准传输控制协议(TCP)实现。
- SOCK_DGRAM :用于发送称为数据报的独立信息包。数据报不保证按发送顺序到达,甚至不保证能到达。例如美国邮政系统,每封信都是一个独立消息,信件可能不按发送顺序到达,甚至可能丢失。在 Internet 域中,使用 Internet 标准用户数据报协议(UDP)实现。
- protocol :指定套接字应使用的协议号,通常与地址族相同。在大多数情况下,可以指定为 0,系统会自动确定。

如果套接字创建成功,将返回一个套接字描述符,这是一个小的非负整数,类似于文件描述符,但语义略有不同。如果创建失败,返回 -1,并将错误信息存储在 errno 中。

6. 服务器端函数

服务器进程要与客户端交换数据,需要按顺序调用以下函数。
- 命名套接字 :创建套接字后,服务器进程必须为其分配一个名称,否则客户端程序无法访问。使用 bind 函数:

#include <sys/types.h>
#include <sys/socket.h>

int bind(int s, const struct sockaddr *name, int addrlen);

该函数将套接字描述符 s 引用的通信通道赋予 name 描述的地址。为使 bind 成功,该地址不能已被使用。由于 name 的大小可能因地址族而异,使用 addrlen 指定其长度。成功时返回 0,失败(通常是因为地址已被使用)时返回 -1,并将错误代码存储在 errno 中。

在 Internet 域中, name 参数实际上是 struct sockaddr_in 类型,定义在 netinet/in.h 文件中:

struct sockaddr_in {
    short             sin_family;
    u_short           sin_port;
    struct in_addr    sin_addr;
};
  • sin_family :总是设置为 AF_INET ,表示该地址在 Internet 域中。
  • sin_port :与该套接字关联的端口号。
  • sin_addr :与端口关联的主机地址。

编写服务器进程时,要注意主机可能有多个网络接口和多个网络地址。可以创建多个套接字并分别绑定不同的地址,也可以在 sin_addr 元素中使用通配符地址 INADDR_ANY ,这样一个套接字就能接收所有网络接口的数据。

  • 等待连接 :如果服务器通过基于流的套接字提供服务,需要使用 listen 函数通知操作系统它已准备好接受客户端连接:
#include <sys/types.h>
#include <sys/socket.h>

int listen(int s, int backlog);

该函数告诉操作系统服务器已准备好在套接字 s 上接受连接。 backlog 参数指定任何给定时间可以挂起的连接请求数量,大多数操作系统会将其限制为最多 5 个。如果连接请求在挂起连接队列已满时到达,客户端将收到连接拒绝错误。

  • 接受连接 :使用 accept 函数接受连接:
#include <sys/types.h>
#include <sys/socket.h>

int accept(int s, struct sockaddr *name, int *addrlen);

当套接字 s 上有连接请求到达时, accept 将返回一个新的套接字描述符,服务器可以使用该描述符与客户端通信,而原来绑定到知名地址的描述符可以继续用于接受其他连接。如果 name 不为空,操作系统会将客户端的地址存储在那里,并将地址长度存储在 addrlen 中。如果 accept 失败,返回 -1,并将失败原因存储在 errno 中。

7. 客户端函数

客户端进程要与服务器进程通信,需要按顺序调用以下函数。
- 连接到服务器 :客户端程序使用基于流的套接字连接到服务器时,调用 connect 函数:

#include <sys/types.h>
// 此处原文档未给出 connect 函数完整声明,通常为
#include <sys/socket.h>
int connect(int s, const struct sockaddr *name, socklen_t addrlen);

该函数尝试将套接字 s 连接到 name 指定的服务器地址, addrlen 指定地址的长度。成功连接时返回 0,失败时返回 -1,并将错误信息存储在 errno 中。

综上所述,使用套接字进行网络编程涉及多个方面,包括获取主机信息、处理端口号、处理字节序、创建套接字以及实现服务器和客户端的通信流程。通过合理使用上述介绍的函数和概念,可以构建出高效、稳定的网络应用程序。

使用套接字进行网络编程

8. 数据传输与通信流程总结

在完成套接字的创建、服务器端和客户端的相关配置后,就可以进行数据的传输了。下面通过一个简单的流程图来总结服务器和客户端的通信流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B(服务器创建套接字):::process
    B --> C(服务器绑定地址和端口):::process
    C --> D(服务器监听连接):::process
    D --> E(客户端创建套接字):::process
    E --> F(客户端连接服务器):::process
    F --> G{连接成功?}:::decision
    G -->|是| H(服务器接受连接):::process
    G -->|否| I(客户端处理错误):::process
    H --> J(服务器与客户端通信):::process
    J --> K(通信结束):::process
    K --> L([结束]):::startend

这个流程图展示了服务器和客户端从开始到结束的主要通信步骤。下面详细说明数据传输过程中的一些要点。

9. 数据发送与接收

在服务器和客户端建立连接后,就可以进行数据的发送和接收了。在基于流的套接字( SOCK_STREAM )中,通常使用 send recv 函数;在基于数据报的套接字( SOCK_DGRAM )中,使用 sendto recvfrom 函数。

基于流的套接字
#include <sys/types.h>
#include <sys/socket.h>

// 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • send 函数用于将 buf 中的 len 字节数据发送到套接字 sockfd flags 参数通常设置为 0。
  • recv 函数用于从套接字 sockfd 接收数据到 buf 中,最多接收 len 字节。返回值表示实际接收的字节数,如果返回 0 表示连接已关闭。
基于数据报的套接字
#include <sys/types.h>
#include <sys/socket.h>

// 发送数据报
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
// 接收数据报
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • sendto 函数用于将数据报发送到指定的目标地址 dest_addr
  • recvfrom 函数用于接收数据报,并获取发送方的地址信息存储在 src_addr 中。
10. 错误处理与资源管理

在网络编程中,错误处理和资源管理非常重要。每个函数调用都可能失败,需要检查返回值并处理错误。例如,在创建套接字、绑定地址、接受连接等操作时,如果失败,应该输出错误信息并进行相应的处理。

#include <stdio.h>
#include <errno.h>

// 示例:创建套接字时的错误处理
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket creation failed");
    // 可以进行其他错误处理,如退出程序
    return -1;
}

同时,在使用完套接字后,要及时关闭套接字以释放系统资源。可以使用 close 函数关闭套接字。

#include <unistd.h>

// 关闭套接字
close(sockfd);
11. 多客户端处理

在实际应用中,服务器可能需要同时处理多个客户端的连接。可以使用多线程或多进程的方式来实现。

多进程方式
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define PORT 8080

void handle_client(int client_socket) {
    // 处理客户端请求的代码
    close(client_socket);
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 绑定地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_socket);
        return -1;
    }

    // 监听连接
    if (listen(server_socket, 5) == -1) {
        perror("listen failed");
        close(server_socket);
        return -1;
    }

    while (1) {
        // 接受连接
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("accept failed");
            continue;
        }

        // 创建子进程处理客户端请求
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程
            close(server_socket);
            handle_client(client_socket);
            exit(0);
        } else if (pid > 0) {
            // 父进程
            close(client_socket);
        } else {
            perror("fork failed");
            close(client_socket);
        }
    }

    close(server_socket);
    return 0;
}
多线程方式
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define PORT 8080

void *handle_client(void *arg) {
    int client_socket = *(int *)arg;
    // 处理客户端请求的代码
    close(client_socket);
    pthread_exit(NULL);
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket creation failed");
        return -1;
    }

    // 绑定地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_socket);
        return -1;
    }

    // 监听连接
    if (listen(server_socket, 5) == -1) {
        perror("listen failed");
        close(server_socket);
        return -1;
    }

    while (1) {
        // 接受连接
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("accept failed");
            continue;
        }

        // 创建线程处理客户端请求
        pthread_t thread_id;
        if (pthread_create(&thread_id, NULL, handle_client, &client_socket) != 0) {
            perror("pthread_create failed");
            close(client_socket);
        }
        pthread_detach(thread_id);
    }

    close(server_socket);
    return 0;
}
12. 总结

使用套接字进行网络编程是一个复杂但强大的技术,涉及到多个方面的知识和技能。从获取主机信息、处理端口号和字节序,到创建套接字、实现服务器和客户端的通信流程,再到处理多客户端连接和错误处理,每个环节都需要仔细考虑和实现。通过合理运用上述介绍的函数、概念和编程技巧,可以构建出高效、稳定、可靠的网络应用程序。在实际开发中,还可以根据具体需求进一步优化和扩展这些基本的网络编程模型。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值