在深入探讨 C 语言中的网络编程之前,确保你对 C 语言的基础概念有扎实的理解是至关重要的,这包括但不限于:
1、变量与数据类型:整型(int)、浮点型(float/double)、字符型(char)及指针等。
2、控制结构:条件语句(if-else)、循环结构(for、while、do-while)。
3、函数:定义与调用,参数传递,返回值。
4、数组与字符串:一维数组、多维数组、字符数组与字符串处理函数。
5、结构体:自定义数据类型的创建与使用。
6、文件 I/O:打开、读取、写入和关闭文件的基本操作。
网络编程涉及多个层次的概念,从底层的套接字(Sockets)到高层的应用协议(如 HTTP、FTP),以下是一些核心概念:
1、IP 地址与端口号:每个网络设备都有一个唯一的 IP 地址,而端口号用于区分同一设备上的不同服务。
2、TCP 与 UDP:
TCP(传输控制协议):提供可靠、有序的数据传输,适用于需要确保数据完整性的应用,如网页浏览(HTTP/HTTPS)。
UDP(用户数据报协议):一个简单的传输层协议,提供面向事务,不需要建立连接的数据传输服务,速度快但不如 TCP 可靠。
3、套接字(Sockets):网络通信的端点,允许应用程序发送和接收数据,主要有流套接字(基于 TCP)和数据报套接字(基于 UDP)。
在 C 语言中,进行网络编程主要依赖于 POSIX 标准的套接字 API,以下是一些关键的函数和库:
函数/库 | 描述 |
socket() | 创建一个新的套接字 |
bind() | 将套接字绑定到指定的 IP 地址和端口号 |
listen() | 使套接字进入被动模式,等待连接请求 |
accept() | 接受一个连接请求,返回一个新的套接字描述符 |
connect() | 主动发起连接请求 |
send() /recv() | 发送和接收数据 |
sendto() /recvfrom() | 对于 UDP,直接发送和接收数据报 |
shutdown() /close() | 关闭套接字 |
四、实战案例:简单的 TCP 服务器与客户端
1. TCP 服务器示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 8080 int main() { int server_fd, new_socket; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[1024] = {0}; char hello = "Hello from server"; // 创建套接字文件描述符 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 强制绑定套接字到端口 8080 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr )&address, sizeof(address))<0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, 3) < 0) { perror("listen"); exit(EXIT_FAILURE); } if ((new_socket = accept(server_fd, (struct sockaddr )&address, (socklen_t)&addrlen))<0) { perror("accept"); exit(EXIT_FAILURE); } read(new_socket, buffer, 1024); printf("Message from client: %s ", buffer); send(new_socket, hello, strlen(hello), 0); printf("Hello message sent "); close(new_socket); close(server_fd); return 0; }
2. TCP 客户端示例
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #define PORT 8080 int main() { struct sockaddr_in serv_addr; int sock = 0; char hello = "Hello from client"; char buffer[1024] = {0}; if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf(" Socket creation error "); return -1; } memset(&serv_addr, '0', sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) { printf(" Invalid address/ Address not supported "); return -1; } if (connect(sock, (struct sockaddr )&serv_addr, sizeof(serv_addr)) < 0) { printf(" Connection Failed "); return -1; } send(sock, hello, strlen(hello), 0); printf("Hello message sent "); int valread = read(sock, buffer, 1024); printf("%s ",buffer ); close(sock); return 0; }
Q1: 如何在 C 语言中处理网络字节序转换?
A1: 在网络编程中,由于不同的计算机体系结构可能采用不同的字节序(大端或小端),因此在发送和接收数据时需要进行字节序的转换,C 语言提供了htonl()
(主机到网络长整型)、htons()
(主机到网络短整型)、ntohl()
(网络到主机长整型)和ntohs()
(网络到主机短整型)这几个函数来进行转换,在发送一个整数前,应先使用htonl()
将其转换为网络字节序。
Q2: 为什么使用SO_REUSEADDR
选项?
A2:SO_REUSEADDR
是一个套接字选项,它允许服务器快速重启而不会因为“地址已在使用”的错误而失败,当一个套接字被关闭后,操作系统会保留该端口一段时间以防止新的套接字立即使用相同的端口,通过设置SO_REUSEADDR
,可以覆盖这一行为,使得新套接字能够立即绑定到之前使用的端口上,这对于开发和测试非常有用,不过,在生产环境中使用时需谨慎,因为它可能导致未预期的行为,比如旧连接的数据被新连接意外接收。