《C 语言 Socket 网络编程:深入剖析与实战》

ops/2024/12/25 23:13:04/

目录

一、引言

二、Socket 网络编程基础

(一)Socket 概念

(二)网络协议与 Socket 类型

(三)IP 地址与端口号

三、C 语言 Socket 编程实战步骤

(一)TCP 服务器端编程

(二)TCP 客户端编程

(三)UDP 服务器端编程

(四)UDP 客户端编程

四、常见问题与解决方案

(一)连接超时问题

(二)数据丢失或乱序问题(针对 UDP)

五、Socket 网络编程优化技巧

(一)缓冲区优化

(二)异步 I/O

六、实际案例应用

(一)简单的 Web 服务器

(二)多人在线聊天系统

七、总结

图示

TCP服务端和客户端创建流程

UDP服务端和客户端创建流程


一、引言

        在当今数字化时代,网络已然渗透到生活的方方面面,从日常的网页浏览、即时通讯到复杂的分布式系统、云计算等领域,网络编程起着关键作用。而 C 语言作为一门经典且高效的编程语言,其提供的 Socket 编程接口为开发者搭建起了实现网络通信的坚实桥梁。本文将深入探讨 C 语言下的 Socket 网络编程,涵盖基础概念、编程步骤、优化技巧以及实际案例应用,助力读者掌握这一强大的网络开发工具。

二、Socket 网络编程基础

(一)Socket 概念

Socket,通俗来讲,就是网络上不同进程间进行双向通信的端点,类似于电话系统中的插座。它屏蔽了底层复杂的网络协议细节,使得应用程序能够便捷地在网络环境中发送和接收数据。在 C 语言中,我们通过调用系统提供的 Socket 相关函数来创建、操作这些通信端点。

(二)网络协议与 Socket 类型

  1. TCP(传输控制协议)
    • TCP 是一种面向连接的、可靠的传输协议。基于 TCP 的 Socket 提供字节流服务,确保数据在传输过程中不丢失、无差错、按序到达接收端。这就如同邮寄挂号信,每一封信都有跟踪记录,丢失会补发。常用于文件传输、网页浏览、电子邮件等对数据准确性要求极高的场景。
    • 基于 TCP 的 Socket 在 C 语言编程中,使用 SOCK_STREAM 套接字类型。
  2. UDP(用户数据报协议)
    • UDP 则是无连接的、不可靠的传输协议。它以数据报为单位进行传输,数据发送出去后不保证一定能到达接收端,也不保证顺序,但传输速度快、开销小。类似于发送普通明信片,没有回执,丢了就丢了。适用于实时性要求高、对少量数据丢失不敏感的应用,如视频直播、在线游戏中的实时位置更新等。
    • 基于 UDP 的 Socket 在 C 语言编程中,使用 SOCK_DGRAM 套接字类型。

(三)IP 地址与端口号

  1. IP 地址:是网络上设备的唯一标识,分为 IPv4(32 位,如常见的 192.168.0.1)和 IPv6(128 位,格式更为复杂)。IP 地址确定了数据传输的目标主机位置。
  2. 端口号:用于标识一台主机上的特定进程。范围是 0 - 65535,其中 0 - 1023 被系统服务保留,如 HTTP 的 80 端口、HTTPS 的 443 端口;1024 - 49151 是注册端口,供普通应用程序注册使用;49152 - 65535 是动态或私有端口,常被临时分配。例如,当浏览器访问网页时,它会连接到服务器的 80 端口(假设为 HTTP 协议),服务器上运行的 Web 服务进程监听在此端口,接收来自浏览器的请求。

三、C 语言 Socket 编程实战步骤

(一)TCP 服务器端编程

如图所示:

1.创建 Socket:使用 socket 函数创建一个基于 IPv4(AF_INET)、面向连接的流套接字(SOCK_STREAM)。示例代码如下:

#include <sys/types.h>
#include <sys/socket.h>
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {perror("Socket creation failed");exit(1);
}

这里,如果 socket 函数返回 -1,表示创建失败,通过 perror 函数输出错误信息并终止程序。
2. 绑定 IP 地址和端口号:将创建好的 Socket 绑定到指定的 IP 地址和端口号。首先要填充 struct sockaddr_in 结构体,设置好地址族、端口号(需转换为网络字节序)、IP 地址(可设为 INADDR_ANY 表示监听本机所有可用 IP 地址),然后调用 bind 函数。示例:

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);  // 假设监听端口为8888
server_addr.sin_addr.s_addr = INADDR_ANY;
memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("Bind failed");close(server_socket);exit(1);
}

同样,绑定失败时要妥善处理错误,关闭已创建的 Socket。
3. 监听连接请求:调用 listen 函数,让服务器进入监听状态,等待客户端连接。参数指定了最大连接数。例如:

if (listen(server_socket, 5) == -1) {perror("Listen failed");close(server_socket);exit(1);
}

4. 接受客户端连接:使用 accept 函数阻塞等待客户端连接,当有客户端连接时,返回一个新的 Socket(用于与该客户端通信)和客户端地址。示例:

struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_size);
if (client_socket == -1) {perror("Accept failed");close(server_socket);exit(1);
}

此时,可以获取客户端的 IP 地址和端口号进行打印等操作,以了解连接情况。
5. 数据收发:通过 client_socket 与客户端进行数据的发送和接收。接收数据使用 recv 函数,发送数据使用 send 函数。例如:

char buffer[1024];
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {buffer[bytes_received] = '\0';printf("Received from client: %s", buffer);const char *response = "Hello, Client!";ssize_t bytes_sent = send(client_socket, response, strlen(response), 0);if (bytes_sent == -1) {perror("Send failed");} else {printf("Response sent to client successfully.\n");}
}

6. 关闭 Socket:通信结束后,依次关闭与客户端通信的 Socket 和服务器监听的 Socket,释放资源。示例:

close(client_socket);
close(server_socket);

(二)TCP 客户端编程

1. 创建 Socket:与服务器端类似,创建一个基于 IPv4、面向连接的流套接字。示例:

int client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {perror("Socket creation failed");exit(1);
}

2. 连接服务器:填充服务器地址结构体,调用 connect 函数连接到指定的服务器 IP 地址和端口号。示例:

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);  // 假设连接到端口8888的服务器
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 假设服务器IP为本地回环地址if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("Connect failed");close(client_socket);exit(1);
}

连接失败时要及时处理,关闭已创建的 Socket。
3. 数据收发:客户端同样使用 send 和 recv 函数与服务器进行数据交互。例如:

const char *message = "Hello, Server!";
ssize_t bytes_sent = send(client_socket, message, strlen(message), 0);
if (bytes_sent == -1) {perror("Send failed");
}char buffer[1024];
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {buffer[bytes_received] = '\0';printf("Received from server: %s", buffer);
}

4. 关闭 Socket:通信结束后,关闭客户端 Socket。示例:

close(client_socket);

(三)UDP 服务器端编程

UDP 编程与 TCP 编程有相似之处,但也存在关键差异。

1. 创建 Socket:使用 socket 函数创建基于 IPv4、数据报套接字(SOCK_DGRAM)。示例:

int server_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (server_socket == -1) {perror("Socket creation failed");exit(1);
}

2. 绑定 IP 地址和端口号:与 TCP 类似,填充结构体并绑定。示例:

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);  // 假设监听端口为9999
server_addr.sin_addr.s_addr = INADDR_ANY;
memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("Bind failed");close(server_socket);exit(1);
}

3. 数据收发:UDP 使用 recvfrom 和 sendto 函数,因为需要处理来自不同客户端的数据包,要明确收发的源地址和目的地址。示例:

char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
ssize_t bytes_received = recvfrom(server_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_size);
if (bytes_received > 0) {buffer[bytes_received] = '\0';printf("Received from client: %s", buffer);const char *response = "Hello, UDP Client!";ssize_t bytes_sent = sendto(server_socket, response, strlen(response), 0, (struct sockaddr *)&client_addr, client_addr_size);if (bytes_sent == -1) {perror("Sendto failed");} else {printf("Response sent to client successfully.\n");}
}

4. 关闭 Socket:通信结束后关闭 Socket。示例:

close(server_socket);

(四)UDP 客户端编程

1. 创建 Socket:创建基于 IPv4、数据报套接字。示例:

int client_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (client_socket == -1) {perror("Socket creation failed");exit(1);
}

2. 数据收发:同样使用 sendto 和 recvfrom 函数,在发送时要指定服务器的地址和端口,接收时获取源地址信息。示例:

const char *message = "Hello, UDP Server!";
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);  // 假设服务器端口为9999
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 假设服务器IP为本地回环地址ssize_t bytes_sent = sendto(client_socket, message, strlen(message), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (bytes_sent == -1) {perror("Sendto failed");
}char buffer[1024];
struct sockaddr_in server_reply_addr;
socklen_t server_reply_addr_size = sizeof(server_reply_addr);
ssize_t bytes_received = recvfrom(client_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&server_reply_addr, &server_reply_addr_size);
if (bytes_received > 0) {buffer[bytes_received] = '\0';printf("Received from server: %s", buffer);
}

3. 关闭 Socket:通信结束后关闭 Socket。示例:

close(client_socket);

四、常见问题与解决方案

(一)连接超时问题

  1. 问题描述:在 TCP 客户端连接服务器时,如果服务器未响应或网络异常,客户端可能会长时间处于阻塞状态等待连接,影响用户体验。
  2. 解决方案:可以设置套接字的超时时间。在 C 语言中,使用 setsockopt 函数结合 SO_RCVTIMEO 和 SO_SNDTIMEO 选项分别设置接收和发送超时时间。示例:
struct timeval timeout;
timeout.tv_sec = 5;  // 设置超时时间为5秒
timeout.tv_usec = 0;if (setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == -1) {perror("Setsockopt failed");
}
if (setsockopt(client_socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == -1) {perror("Setsockopt failed");
}

这样,当客户端在发送或接收数据超过 5 秒未完成时,send 或 recv 函数将返回错误,可根据错误码进行相应处理,避免无限期阻塞。

(二)数据丢失或乱序问题(针对 UDP)

  1. 问题描述:由于 UDP 的不可靠性,数据报在网络传输中可能丢失或到达接收端时顺序错乱,对于一些对数据完整性要求较高的应用场景会造成困扰。
  2. 解决方案:在应用层实现简单的确认和重传机制。例如,客户端发送数据后,启动一个定时器,等待服务器的确认数据包。如果在规定时间内未收到确认,重新发送数据。服务器收到数据后,立即发送确认包给客户端。在 C 语言中,可以利用 select 函数结合定时器来实现这种机制,同时维护发送数据的缓冲区和状态信息,以便重发。

五、Socket 网络编程优化技巧

(一)缓冲区优化

1. 合理设置接收和发送缓冲区大小:根据应用需求和网络状况,使用 setsockopt 函数的 SO_RCVBUF 和 SO_SNDBUF 选项调整缓冲区大小。例如,如果是大数据量传输的文件服务器,适当增大缓冲区,减少频繁的系统调用开销,提高传输效率。示例:

int buffer_size = 65536;  // 假设设置缓冲区大小为64KB
if (setsockopt(server_socket, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size)) == -1) {perror("Setsockopt failed");
}
if (setsockopt(server_socket, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size)) == -1) {perror("Setsockopt failed");
}

2. 避免缓冲区溢出:在接收数据时,确保接收缓冲区有足够空间,并且对接收的数据长度进行严格校验,防止数据溢出覆盖其他内存区域,导致程序崩溃或安全漏洞。

(二)异步 I/O

采用异步 I/O 模式可以提高程序的并发处理能力,避免线程阻塞等待 I/O 操作完成。在 C 语言中,可以利用 selectpoll 或更高级的 epoll(Linux 系统下)等函数实现异步 I/O。例如,使用 epoll 可以监听多个套接字的事件(可读、可写、异常等),当有事件发生时,及时进行处理,而不是逐个套接字轮询等待,大大提高了服务器的响应速度和吞吐量。不过,异步 I/O 的实现相对复杂,需要深入理解事件驱动模型和相关函数的使用。

六、实际案例应用

(一)简单的 Web 服务器

利用 TCP Socket 实现一个基础的 Web 服务器,监听 80 端口(或其他指定端口)。当客户端(浏览器)发送 HTTP 请求时,服务器解析请求,查找对应的本地文件资源(如 HTML、CSS、JS 文件等),如果找到,将文件内容封装成 HTTP 响应格式(包含状态码、头部信息、正文等),通过 Socket 发送回客户端,实现简单的网页浏览功能。这个案例涉及到 HTTP 协议的基本解析、文件读取和 Socket 数据传输的综合运用。

(二)多人在线聊天系统

基于 UDP 或 TCP 构建一个多人在线聊天系统。如果采用 TCP,服务器作为中心节点,负责维护客户端连接列表,接收客户端发送的聊天消息,并将消息转发给其他在线客户端。客户端通过 Socket 连接到服务器,发送和接收聊天信息,实现实时互动。若使用 UDP,服务器同样监听特定端口,接收来自不同客户端的数据包,由于 UDP 的特性,需要更注重消息的可靠性处理,如添加序列号、确认机制等,以保障聊天的顺畅进行。这个案例能充分体现 Socket 在实时通信场景中的应用以及不同协议的特点。

七、总结

图示

TCP服务端和客户端创建流程

服务端:

1、创建套接字socket

2、绑定地址和端口(bind)

3、开始监听(listen)

4、等待客户端连接(accept)

5、接收和发送数据(receive、send)

6、关闭套接字(close)

客户端:

1、创建套接字(socket)

2、连接服务器(connect)

3、发送和接收数据(send、receive)

4、关闭套接字(close)

UDP服务端和客户端创建流程

服务端:

1、创建套接字(socket)

2、绑定地址和端口(bind)

3、接收和处理数据(recvfrom)

4、发送数据(send)

5、关闭套接字(close)

客户端:

1、创建套接字(socket)

2、发送数据(send)

3、接收数据(recvfrom)

4、关闭套接字(close)


http://www.ppmy.cn/ops/144957.html

相关文章

南海区2021年C++甲组真题第3题——Excel地址

题目描述 Excel单元格的地址表示很有趣&#xff0c;它使用字母来表示列号。 比如&#xff1a; A表示第1列&#xff0c; B表示第2列&#xff0c; Z表示第26列&#xff0c; AA表示第27列&#xff0c; AB表示第28列&#xff0c; BA表示第53列&#xff0c; ... 当然Excel的最大列…

day14-补充静态网卡配置

修改网络模式&#xff0c;修改静态ip&#xff0c;动态ip获取方式 查看当前的上网信息 1.确保你的机器&#xff0c;是连接的网络的&#xff0c;是插上了网线的。&#xff08;模拟了物理服务器的软件是什么&#xff1f;看你的虚拟的机器&#xff08;vmware&#xff09;&#xf…

若依启动项目时配置为 HTTPS 协议

文章目录 1、需求提出2、应用场景3、解决思路4、注意事项5、完整代码第一步&#xff1a;修改 vue.config.js 文件第二步&#xff1a;运行项目第三步&#xff1a;处理浏览器警告 6、运行结果 1、需求提出 在开发本地项目时&#xff0c;默认启动使用的是 HTTP 协议。但在某些测试…

python1:开发环境及配置

1&#xff0c;win pc本地安装&#xff1a; 课上选用的是清华大学开源软件镜像站 主要是python解释器&#xff0c;除了上面的anaconda数据科学套件&#xff0c; 我课上还推荐学生使用vscode或pycharm 2&#xff0c;如果是在linux系统服务器上&#xff1a; 配置就更加简单了 vs…

封装(3)

大家好&#xff0c;今天我们来学习一下静态方法相关的内容&#xff0c;这个要和普通成员做一个区分&#xff0c;那么它们到底有什么不同点呢&#xff0c;我们现在就来看看。 7.2static修饰成员变量 1、访问方式,通过类名 静态变量不在对象里面,在方法区,要通过类名.访问. st…

jvm接入prometheus监控

创建以下两个配置类&#xff1a; package com.haoze.doctor.config; import com.alibaba.druid.pool.DruidDataSource; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotat…

AIGC-----AIGC在虚拟现实中的应用前景

AIGC在虚拟现实中的应用前景 引言 随着人工智能生成内容&#xff08;AIGC&#xff09;的快速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的应用也迎来了新的契机。AIGC与VR的结合为创造沉浸式体验带来了全新的可能性&#xff0c;这种组合不仅极大地降低了VR内容的…

最适合智能体的身份认证技术:对比OpenID Connect、API keys、did:wba

最适合智能体的身份认证技术&#xff1a;对比OpenID Connect、API keys、did:wba 智能体需要新的身份认证技术 智能体对身份认证技术提出了新的需求&#xff0c;其中最重要的一个就是互联互通&#xff0c;特别是让任意两个智能体都能够互联互通。 其中的原理很简单&#xff1a;…