unix网络编程
再过几年,机器人发展起来,还能在流水线呆下去吗。
更新ing
参考了学长的笔记
文章目录
0. 计算机网络基础
API:作为应用层与传输层之间的接口 分隔用户进程与内核 使应用不需要关心通信细节。
A类网段:1.0.0.0 到 126.0.0.0。
默认子网掩码:255.0.0.0。
网络数量:可用的A类网络有126个。
主机数量:每个网络能容纳约1亿多个主机。
B类网段:128.0.0.0 到 191.255.255.255。
默认子网掩码:255.255.0.0。
网络数量:可用的B类网络有16382个。
主机数量:每个网络能容纳约6万多个主机。
C类网段:192.0.0.0 到 223.255.255.255。
默认子网掩码:255.255.255.0。
网络数量:可用的C类网络有209万个。
主机数量:每个网络能容纳254个主机。
D类网段:224.0.0.0 到 239.255.255.255。
用途:用于多点广播(Multicast),不分配给特定网络。
A类私有网段的范围是10.0.0.0到10.255.255.255。
B类私有网段的范围是172.16.0.0到172.31.255.255。
C类私有网段的范围是192.168.0.0到192.168.255.255。
“众所周知”的端口:0~1023,由IANA(因特网分配数值权威机构)统一控制
注册的端口:1024~49151,这些端口虽不由IANA控制,但IANA登记这些端口的使用
动态或私有的端口:49152~65535
TCP协议数据段格式
HLEN:首部长度,以4字节(32位)为单位。tcp数据段首部包括固定和变长两部分;
窗口:为通告窗口;
URG位:如果使用紧急数据指针,则将这一位设为1
ACK位:如果确认序列号有效,则设为1;
PSH位:表示”推”数据,如果这一位设置成1,表示希望接收方在接收到这个数据段之后,将它立即传送给高层应用程序,而不是缓存起来。
RST位:表示请求重置连接。当TCP协议接收到一个不能处理的数据段时,向对方TCP协议发送这种数据段,表示这个数据段所标识的连接出现了某种错误,请求对方TCP协议将这个连接清除。
SYN位:请求建立连接。tcp用这种数据段向对方tcp协议请求建立连接,在这个数据段中,tcp协议将它选择的初始序列号通知对方,并且与对方协议协商最大数据段的大小。
FIN位:请求关闭连接。当协议收到对这个数据段的确认后,成功关闭写方向的连接,因为tcp连接是全双工的,在发送了FIN数据段之后,它仍能接收数据,直至对方也发送FIN数据段。
TCP三次握手
TCP四次挥手
存在TIME_WAIT状态有两个理由:
实现终止TCP全双工连接的可靠性(假设最后一个ack丢失的情况).
允许老的重复分节在网络中消逝.
1. ip地址与端口号
常见常量
//地址族af
AF_INET(IPv4)
AF_INET6(IPv6)
表示 IPv4 地址和端口号的结构体
#include <stdio.h>
#include <arpa/inet.h>
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常是 AF_INET(IPv4)
in_port_t sin_port; // 端口号(16 位,网络字节序)
struct in_addr sin_addr; // IPv4 地址(32 位,网络字节序)
char sin_zero[8]; // 填充字段,通常设置为 0
};
struct in_addr {//用于表示ipv4
in_addr_t s_addr; // 32 位 IPv4 地址(网络字节序)
//in_addr_t 通常是 uint32_t(32 位无符号整型)。
};
struct sockaddr{//可以表示IPv4或IPv6
__SOCKADDR_COMMON (sa_); //unsigned short int,它的值包括三种:AF_INET,AF_INET6和AF_UNSPEC
/* Common data: address family and length. */
char sa_data[14]; /* Address data. 地址+端口号 */
};
表示 IPv6 地址和端口号的结构体
#include <netinet/in.h>
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,IPv6 是 AF_INET6
in_port_t sin6_port; // 端口号
uint32_t sin6_flowinfo; // IPv6 流信息,用于标识数据流的优先级和 QoS(服务质量)
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 接口范围 ID,用于标识链路本地地址的接口,通常为0
};
struct in6_addr {
union {
uint8_t __u6_addr8[16]; // 16 个 8 位无符号整数
uint16_t __u6_addr16[8]; // 8 个 16 位无符号整数
uint32_t __u6_addr32[4]; // 4 个 32 位无符号整数
} __in6_u; // 联合体,用于以不同方式访问 IPv6 地址
};
ip地址字符串格式与二进制格式互转(均支持ipv4)
函数 | 功能 | 错误返回值 | 支持 IPv6 | 线程安全 |
---|---|---|---|---|
in_addr_t inet_addr(const char *cp); | 字符串 → 二进制 | -1 | ❌ | ✔️ |
int inet_pton(int af, const char *src, void *dst); | 字符串 → 二进制 | 0或-1 | ✔️ | ✔️ |
int inet_aton(const char *cp, struct in_addr *inp); | 字符串 → 二进制 | 0 | ❌ | ✔️ |
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); | 二进制 → 字符串 | null | ✔️ | ✔️ |
char *inet_ntoa(struct in_addr in); | 二进制 → 字符串 | 指向静态缓冲区的指针 | ❌ | ❌ |
端口号网络字节序(大端序)与主机字节序互相转换
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); //将主机字节序的端口号转换为网络字节序。
uint16_t ntohs(uint16_t netshort); //将网络字节序的端口号转换为主机字节序。
uint32_t htonl(uint32_t hostlong) //用于将主机字节序转换为网络字节序。
uint32_t ntohl(uint32_t netlong) //将网络字节序的端口号转换为主机字节序。
2. TCP与UDP
1. TCP
在TCP服务器编程中,当你在创建一个socket时将端口号设置为0,这意味着操作系统会自动为你的socket分配一个可用的端口。
//创建套接字描述符并返回
int socket (int __domain, int __type, int __protocol);//<0失败
//domain指定协议族 AF_INET AF_INET6 AF_ROUTE(路由套接字)
//type指定套接字类型 SOCK_STREAM(字节流TCP) SOCK_DGRAM(数据包UDP) SOCK_RAW(原始套接字)
//protocol参数只在原始套接字类型中需要赋值,否则为0即可
//绑定套接字描述符到端口
int bind (int __fd, const sockaddr* __addr, socklen_t __len)//<0失败
//fd为socket函数返回值,addr为服务器地址
//len为地址结构的长度
//转换套接字为监听套接字
int listen (int __fd, int __n);//<0失败
//fd指定套接字描述符
//n指定请求队列的最大连接数
//接收客户端连接返回一个已连接套接字
int accept (int __fd, sockaddr* __restrict__ __addr, socklen_t * __restrict__ __addr_len);//<0失败
//fd为监听套接字,addr用来接收客户端的地址结构
//addr_len为该地址结构的大小
//客户端连接到服务器端,成功则fd变成已连接套接字
int connect (int __fd, const sockaddr* __addr, socklen_t __len);//<0失败
//addr传入服务器地址
//发送数据,返回数据字节数
ssize_t write (int __fd, const void *__buf, size_t __n);
//fd为已连接套接字,buf为发送数据的缓冲区
//len指定发送数据缓冲区大小
//接收数据,返回值:>0成功,=0关闭,<0出错
ssize t read(int fd,void buf,size_t nbytes);
//发送数据
ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);
//flags为传输控制标志,一般为0
//接收数据
ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
//关闭套接字,关闭之前会将待发送数据发送完,实质上是把套接字描述符的访问数-1
int close (int __fd);
//当访问计数=0时,才会触发TCP协议中的终止连接(四次挥手)操作
套接字地址结构赋值
struct sockaddr_in address;
address.sin_family = AF_INET;//设置地址族为IPv4
address.sin_addr.s_addr = INADDR_ANY;//设置服务器IP地址为INADDR_ANY,表示监听所有可用的网络接口
address.sin_port = htons(PORT);//设置端口号,并使用htons函数将端口号转换为网络字节序
//if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
获取地址信息
//用于获取与套接字连接的对端地址信息
int getpeername(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __len);
//fd为已连接套接字,addr存储对端地址信息,len为地址结构的大小
//获取本地套接字地址
int getsockname(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __len);
2. UDP
UDP长度最小是8
UDP缺乏可靠性支持,应用程序必须实现:确认、超时、重传、流控等
// 发送数据
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);
//前四个参数与recv一致
//src_addr参数 为通用套接字地址结构,用于返回消息来源的地址。
//addrlen 传入时指定src_addr结构的长度,执行后为消息来源地址的长度。
//src_addr参数可以为空指针,此时addrlen参数也必须是空指针,表示不关心数据方的协议地址。
响应验证
由于向UDP进程发送数据不需要建立链接,导致任何进程都有可能向客户发送数据,因此需要判断响应是否来自客户端
if(len != sizeof(server)
||memcmp((const void *)&server, (const void *)&peer, len) != 0)
{
printf("来自不是服务器的地方");
}
else{
printf("来自服务器");
}
3. 进程线程协程管程
1. 进程
进程是操作系统资源分配的基本单位,是程序的一次执行实例。
特点
独立性:拥有独立的地址空间、文件描述符、环境变量等
隔离性:一个进程崩溃不会影响其他进程
资源开销大:创建、销毁和切换成本高
通信复杂:需要IPC机制(管道、消息队列、共享内存等)
典型应用
需要高安全性和稳定性的任务
浏览器多标签页(现代浏览器通常每个标签页一个进程)
操作系统中的各种守护进程
多进程:并行计算技术,特别适用于
1.需要充分利用多核CPU资源的场景,如科学计算,数据分析,图像处理。由于Python的GIL(全局解释器锁)限制,多线程在CPU密集型任务中的性能提升有限,而多进程则能有效绕过这一限制,充分利用多核CPU资源。
2.故障隔离:多进程可以提供内存隔离,关键模块可放在独立进程中避免可防止一个组件崩溃就导致整个应用或服务崩溃。
3.需要大内存操作的服务或组件:多进程可以独立回收资源,确保资源的高效回收和管理。
fork操作会使子进程继承父进程所有打开的文件描述符,包括数据库连接套接字。若父进程关闭连接,子进程仍持有原描述符副本,可能导致"使用已关闭连接"错误。并且并发(实际不是同一时间运行)操作同一事务可能导致数据不一致或死锁。
应用多进程的中间件:nginx,Apache Prefork
创建进程
fork()
- 采用 Copy on write 的资源拷贝方式,在进程试图写数据之前,两个进程对数据共享。在
进程写数据之后,会拷贝一份数据,以避免子父进程之间互相影响。- 父子进程执行顺序不确定,由调度器决定
vfork()
- 父子进程共享使用数据,一个进程对数据的更改会影响到另一个数据的执行
- 强制子进程先运行,父进程阻塞直到子进程调用
exec()
或exit()
调用fork函数或vfork函数成功后,会产生两个返回值。其中,子进程的返回值是0,而父进程的返回值 是子进程的id号。
// 多进程并发服务器创建子进程处理客户端请求
if ((pid = fork()) > 0) {//父进程
close(connfd);// 父进程关闭连接套接字
}else if (pid == 0) { // 子进程
close(listenfd); // 子进程关闭监听套接字
handle_client(connfd, cliaddr);
exit(0);
}
else {
printf("fork error!\n");
exit(1);
}
终止进程
僵尸进程:已经终止,但父进程尚未对其进行善后处理
pid_t wait(int *statloc); // 立即返回,并释放已终止子进程的资源。
//若没有已终止的子进程,但有正在执行的子进程,则会阻塞在这里。
//若没有子进程,则出错并返回-1
//成功执行会返回终止子进程的ID
//stat_loc参数返回子进程的终止状态。
//若多个子进程同时终止,wait()函数可能只会执行一次,导致出现僵尸进程。
//使用waitpid()函数可以通过更精确的控制来避免这个问题。
pid_t waitpid(pid_t pid,int *statloc,int option);
//pid指明关注哪些子进程。pid>0:关注进程号为pid的进程;
//pid<-1:关注进程组号为|pid|的所有进程; pid=-1:关注所有进程
//statloc参数与wait函数相同
//option参数指定附加选项,如WNO_HANG:内核中无已终止进程时不阻塞。
2. 线程
线程是CPU调度的基本单位,是进程内的一个执行流。
特点
共享资源:线程共享所属进程的资源,没有独立的虚拟空间,但有栈,PC,本地存储等独立空间。
轻量级:创建、销毁和切换比进程快
同步需求:需要同步机制(互斥锁、信号量等)保护共享数据
通信简单:可直接通过共享内存通信
典型应用
需要并发执行的应用程序
Web服务器处理多请求
GUI应用程序保持界面响应
多线程适用于
1.IO密集型操作:如同时读写多个文件,爬虫下载网页资源。
2.异步任务处理:某些任务不需要立即返回结果,可以通过多线程异步执行,避免阻塞主线程,比如用户注册后发送欢迎邮箱,文件上传后异步处理文件(压缩,转换)。
3.高并发服务:HTTP服务器为每个请求分配独立线程,实现同时响应上千用户。Tomcat等容器通过线程池管理请求处理,提升吞吐量
应用多线程的中间件:redis,tomcat
//创建新线程
//执行该函数后,一个新线程将会被创建,并开始执行线程函数中的代码。
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void *(*func)(void *),void *arg);
//tid:返回创建线程的id号
//attr:指向线程属性,通常设置为NULL
//func:线程函数,子线程将要执行的代码需要封装在一个函数中,称为线程函数。该函数的返回值必须是一个void类型的指针,它的参数也必须是有且仅有一个void类型的指针。
//arg:参数指针,当线程函数需要输入参数时,需要将参数打包成一个结构,然后通过arg指向这个结构,来进行传参。
//等待线程终止,与waitpid函数功能类似。
//等待的线程必须是当前进程的成员,且不能是分离的线程和守护线程。
int pthread_join(pthread_t tid, void **status);
//将某个线程变为分离的,分离的线程在终止后,会被系统释放拥有的所有资源。
int pthread_detach(pthread_t tid);
//获取线程自己的ID号
int pthread_self();
//终止当前线程
void pthread_exit();
传递参数方式(3种)
-
通过强制类型转换,把要传递的数据直接转换为void *类型,传入后再转换回来
缺点:只能传递一个参数 -
主线程创建arg局部结构体变量,并给其赋值。用一个指针指向该结构并传入。
缺点:某个线程对arg的修改会影响到其他进程。 -
主线程为arg结构体malloc分配空间并产生指针,将该指针传入。
3. 协程管程
协程是用户态的轻量级线程,由程序员在用户空间控制调度。
特点
协作式调度:主动让出执行权,而非被系统抢占
极轻量级:上下文切换开销极小
无并行性:单线程内通过协作实现并发
高并发能力:可支持大量(数十万)并发
典型应用
高并发网络服务
异步I/O编程
生成器/迭代器实现
Python的asyncio、Go的goroutine
管程是一种高级同步原语,用于管理对共享资源的访问。
特点
封装性:将共享数据和操作封装在一起
互斥访问:自动保证同一时间只有一个线程执行管程代码
条件变量:提供wait/signal机制进行线程协调
语言级支持:通常需要编程语言直接支持(如Java的synchronized)
典型应用
实现线程安全的共享数据结构
生产者-消费者问题
读者-写者问题
各种需要同步的并发场景
4. 迭代与并发
迭代服务器(Iterative Server):
- 顺序处理客户端请求
- 一次只处理一个客户端连接
- 当前请求处理完成后才接受下一个连接
并发服务器(Concurrent Server):
- 同时处理多个客户端请求
- 可以并行服务多个客户端
- 使用多进程/多线程或I/O多路复用技术实现并发
多进程并发服务器如下
5. 线程安全
TSD 即线程特定数据。
类似于全局变量,但是是线程私有的,是定义线程私有数据的唯一方法。
以关键字标志,通过函数进行读写。
pthread_once: 确保某个函数在整个程序中只被执行一次,即使多个线程同时调用它。
pthread_getspecific: 获取与当前线程关联的特定数据键(假设1)的值(pkey[1])。
pthread_create: 创建一个新的线程,并指定该线程要执行的函数。
pthread_detach: 将该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。或者进程的main函数执行完返回,也会终止终止该进程中所有线程,并释放其使用的资源。
pthread_setspecific: 对应所创建的线程特定数据指针(假设pkey[1])指向它刚刚分配的内存区。
6. IO复用
select函数等待多个事件中的任意一个发生,或等待一定时间,返回准备好描述字的数量。
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timeval *timeout);
//maxfd:指等待集合中所有描述符的范围,即所有描述符的最大值加1
//readset:存放检测“读”条件的描述符集合
//writeset:存放检测“写”条件的描述符集合
//exceptset:存放检测“异常”条件的描述符集合。
//若是对上面的某个条件不感兴趣,将其设置为空指针
//这些参数传入时指定要关注哪些描述符,返回时返回关注的哪些已经准备好。
//timeout:指定等待时间,结构可以设置秒与毫秒 空指针为永远等待,0为轮询
如果select的三个测试指针为空,将提供一个比函数sleep更为精确的定时器(sleep睡眠以秒为最小单位)
select的三个描述字集合分别指示不同测试类型的描述字集合(读、写、异常描述字),其中异常描述字支持:套接字上带外数据的到达和控制状态信息的存在。
Unix下可用的I/O模型
阻塞i/o
非阻塞i/o
i/o复用(select和poll)
信号驱动i/o(SIGIO)
异步i/o(posix.1的aio_系列函数)(适用于I/O密集的应用程序,比如Web应用经常执行网络操作)
UDP服务器客户端
服务器端:
- 创建UDP套接字
- 绑定到指定端口
- 接收客户端消息并打印
- 将消息原样返回给客户端(echo)
客户端:
- 创建UDP套接字
- 向服务器发送消息
- 接收并打印服务器的响应
UDP服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 1024
#define PORT 8080
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 配置服务器地址
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_addr.s_addr = INADDR_ANY; // 接受任意IP的连接
servaddr.sin_port = htons(PORT); // 端口号
// 绑定套接字到服务器地址
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP Server is listening on port %d...\n", PORT);
socklen_t len;
int n;
len = sizeof(cliaddr);
while (1) {
// 接收来自客户端的数据
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
// 将接收到的数据发回客户端
sendto(sockfd, (const char *)buffer, strlen(buffer),
MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
printf("Echo message sent back to client.\n");
}
close(sockfd);
return 0;
}
UDP客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 1024
#define PORT 8080
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
// 配置服务器地址
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_port = htons(PORT); // 端口号
servaddr.sin_addr.s_addr = INADDR_ANY; // 服务器IP地址
int n;
socklen_t len;
const char *hello = "Hello from client";
// 发送消息到服务器
sendto(sockfd, (const char *)hello, strlen(hello),
MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));
printf("Hello message sent to server.\n");
// 接收服务器的响应
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
MSG_WAITALL, (struct sockaddr *)&servaddr, &len);
buffer[n] = '\0';
printf("Server response: %s\n", buffer);
close(sockfd);
return 0;
多线程并发服务器示例
客户端:
-
从命令行读入服务器的IP地址;并连接到服务器;
-
循环从命令行读入一行字符串,并传递给服务器,由服务器对字符串反转,并将结果返回客户程序,如果用户输入的是quit,则服务器端关闭连接,并且输出客户端历史信息;
-
客户程序显示反转后的字符串;
-
通过使用getsockname和getpeername来获取本地和远程用户的地址信息
服务器端:
-
循环接收客户的连接请求,并显示客户的IP地址和端口号;
-
接收客户传来的字符串,反转后传递给客户;
-
采用多线程并发设计,同时处理多个客户端连接
-
使用线程特定数据TSD保证线程安全
服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#define PORT 8080
#define BUFFER_SIZE 1024
//设置服务器监听的端口号,接收和发送数据的缓冲区大小
// 定义客户端信息结构体
typedef struct {
int socket;
struct sockaddr_in address;
} client_info;
// 字符串反转
void reverse_string(char* str) {
int n = strlen(str);
for (int i = 0; i < n / 2; i++) {
char temp = str[i];
str[i] = str[n - i - 1];
str[n - i - 1] = temp;
}
}
//准备使用TSD保证线程安全(count计数正确)
pthread_key_t key; //全局键
pthread_once_t once = PTHREAD_ONCE_INIT;
//析构函数,线程终止时调用,负责释放该线程通过TSD分配的内存
void destructor(void* value) {
if (value != NULL) {
free(value); // 释放线程特定数据内存
//参数value就是线程通过pthread_setspecific设置的值
}
}
void create_key(){
pthread_key_create(&key,destructor);
//创建全局键,在key数组结构找到第一个未使用的元素(假设key[1])将索引(1)赋值给key
//第二个参数指定析构函数,线程退出时自动清理
}
//保存某个线程客户端历史数据
void save_cli_data(char* revbuf,int len, char* pdata){
pthread_once(&once,create_key);//只执行一次
int* count;
if( (count = (int*)pthread_getspecific(key))== NULL){
//通过一个全局的键(假设1)来访问当前线程的私有数据(pkey[1],它是个指向实际数据的指针)。
//每个线程可以通过相同的键访问自己独立的数据副本,而不会干扰其他线程的数据。
count = (int*)malloc(sizeof(int));
*count = 0; //初始化计数器
pthread_setspecific(key,count);
//对应所创建的线程特定数据指针(假设pkey[1])指向它刚刚分配的内存区。
}
int i = 0;
while(i < len){
pdata[(*count)++] = revbuf[i++];//往历史数据添加本次接收到的数据
}
pdata[*count] = '\0';
}
// 处理客户端连接的线程函数
void* handle_client(void* arg) {
client_info* info = (client_info*)arg;
char buffer[BUFFER_SIZE] = { 0 };
char cli_data[BUFFER_SIZE]={0};//存放客户端发来的历史信息
// 获取客户端地址信息
char client_ip[INET_ADDRSTRLEN];//INET_ADDRSTRLEN的值为16
inet_ntop(AF_INET, &(info->address.sin_addr), client_ip, INET_ADDRSTRLEN);
//将客户端的IP地址从二进制格式转换为可读的字符串格式
printf("Client connected: %s:%d\n", client_ip, ntohs(info->address.sin_port));
// 接收数据并反转
while (1) {
memset(buffer, 0, BUFFER_SIZE);//清空缓冲区
int valread = read(info->socket, buffer, BUFFER_SIZE);//从客户端读取数据到缓冲区
if (valread <= 0) {//读取长度<=0
printf("Client disconnected: %s:%d\n", client_ip, ntohs(info->address.sin_port));
close(info->socket);
break;
}
printf("Received from client: %s:%d, content: %s\n", client_ip, ntohs(info->address.sin_port),buffer);
//ntohs将一个16位数由网络字节顺序转换为主机字节顺序
if (strcmp(buffer, "quit") == 0) {
printf("Client requested to quit: %s:%d ", client_ip, ntohs(info->address.sin_port));
//退出连接前打印历史信息
printf(", history message:%s \n",cli_data );
close(info->socket);
break;
}
//保存历史信息
save_cli_data(buffer,valread, cli_data);
// 反转字符串
reverse_string(buffer);
// 发送反转后的字符串
send(info->socket, buffer, strlen(buffer), 0);
printf("Sent to client: %s:%d, content: %s\n", client_ip, ntohs(info->address.sin_port),buffer);
}
free(arg);
pthread_exit(NULL);
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建 socket 文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {//创建一个TCP套接字
//AF_INET:表示使用IPv4地址。SOCK_STREAM:表示使用面向连接的TCP协议。
perror("Socket failed");
exit(EXIT_FAILURE);
}
// 绑定 socket 到端口
address.sin_family = AF_INET;//设置地址族为IPv4
address.sin_addr.s_addr = INADDR_ANY;//设置服务器IP地址为INADDR_ANY,表示监听所有可用的网络接口
address.sin_port = htons(PORT);//设置端口号,并使用htons函数将端口号转换为网络字节序
if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
//将套接字设置为监听状态,等待客户端连接,3表示等待连接队列的最大长度
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d...\n", PORT);
while (1) {
// 接受新的连接
if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
//接收客户端的连接请求并返回一个新的套接字文件描述符`new_socket`用于与客户端通信
perror("Accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 为每个客户端创建一个新的线程
pthread_t thread_id;
client_info* info = (client_info*)malloc(sizeof(client_info));
info->socket = new_socket;
memcpy(&info->address, &address, sizeof(address));
if (pthread_create(&thread_id, NULL, handle_client, (void*)info) < 0) {
perror("Could not create thread");
free(info);
close(new_socket);
}
pthread_detach(thread_id);
//将该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
//或者进程的main函数执行完返回,也会终止终止该进程中所有线程,并释放其使用的资源
}
close(server_fd);
return 0;
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 1024
int main(int argc, char const* argv[]) {
if (argc != 2) {
printf("Usage: %s <server_ip>\n", argv[0]);
return -1;
}
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = { 0 };
// 创建 socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
// 将 IP 地址从字符串转换为二进制格式
if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 连接到服务器
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
// 获取本地和远程地址信息
struct sockaddr_in local_addr, peer_addr;
socklen_t len = sizeof(local_addr);
getsockname(sock, (struct sockaddr*)&local_addr, &len);
getpeername(sock, (struct sockaddr*)&peer_addr, &len);
char local_ip[INET_ADDRSTRLEN];
char peer_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &local_addr.sin_addr, local_ip, INET_ADDRSTRLEN);
inet_ntop(AF_INET, &peer_addr.sin_addr, peer_ip, INET_ADDRSTRLEN);
printf("Local address: %s:%d\n", local_ip, ntohs(local_addr.sin_port));
printf("Connected to server: %s:%d\n", peer_ip, ntohs(peer_addr.sin_port));
while (1) {
printf("Enter a string (or 'quit' to exit): ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符
// 发送数据到服务器
send(sock, buffer, strlen(buffer), 0);
if (strcmp(buffer, "quit") == 0) {
printf("Closing connection...\n");
break;
}
// 接收服务器返回的数据
memset(buffer, 0, BUFFER_SIZE);
int valread = read(sock, buffer, BUFFER_SIZE);
if (valread <= 0) {
printf("Server disconnected\n");
break;
}
printf("Reversed string from server: %s\n", buffer);
}
close(sock);
return 0;
}
TSD使用流程
多进程并发服务器示例
多进程并发服务器建立过程:
建立连接->服务器调用fork()产生新的子进程->父进程关闭连接套接字,子进程关闭监听套接字->子进程处理客户请求,父进程等待另一个客户连接。
客户端:
客户首先与服务器连接,接着接收用户输入客户端名字,并将该名字发送给服务器,接收用户输入的字符串,发送给服务器,接收服务器返回的经处理后的字符串,并显示之。之后,继续等待用户输入直至用户输入Ctrl+C,终止连接并退出。
服务器端:
服务器采用多进程并发设计,等待客户连接请求,连接成功后显示客户地址,并接收该客户的名字并显示,然后接收来自客户的信息(字符串)并显示,然后将该字符串反转,并将结果送回客户,之后,继续等待接收该客户的信息直至该客户关闭连接。要求服务器具有同时处理多个客户的能力。
服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#define MAXLINE 1024
#define PORT 9876
void str_reverse(char* str, int len) {
int i, j;
char temp;
for (i = 0, j = len - 1; i < j; i++, j--) {
temp = str[i];
str[i] = str[j];
str[j] = temp;
}
}
void handle_client(int connfd, struct sockaddr_in cliaddr) {
char buf[MAXLINE];
char client_name[MAXLINE];
int n;
// 显示客户地址
printf("Connection from %s, port %d\n",
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 接收客户名字
n = read(connfd, client_name, MAXLINE);
if (n <= 0) {
close(connfd);
return;
}
client_name[n] = '\0';
printf("Client name: %s\n", client_name);
while (1) {
n = read(connfd, buf, MAXLINE);
if (n <= 0) {
printf("Client %s disconnected\n", client_name);
break;
}
buf[n] = '\0';
printf("Received from %s: %s\n", client_name, buf);
// 反转字符串
str_reverse(buf, n);
// 发送回客户端
write(connfd, buf, n);
}
close(connfd);
}
int main() {
int listenfd, connfd;
pid_t pid;
socklen_t clilen;
struct sockaddr_in servaddr, cliaddr;
// 创建监听套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(listenfd, 5) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server running on port %d...\n", PORT);
while (1) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
if (connfd < 0) {
perror("accept error");
exit(EXIT_FAILURE);
}
// 创建子进程处理客户端请求
if ((pid = fork()) > 0) {//父进程
close(connfd);// 父进程关闭连接套接字
}else if (pid == 0) { // 子进程
close(listenfd); // 子进程关闭监听套接字
handle_client(connfd, cliaddr);
exit(0);
}
else {
printf("fork error!\n");
exit(1);
}
}
close(listenfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
#define MAXLINE 1024
#define PORT 9876
int sockfd;
void sigint_handler(int signo) {
printf("\nClient terminating...\n");
close(sockfd);
exit(0);
}
int main(int argc, char** argv) {
struct sockaddr_in servaddr;
char buf[MAXLINE];
char client_name[MAXLINE];
int n;
if (argc != 2) {
printf("Usage: %s <IPaddress>\n", argv[0]);
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
perror("inet_pton error");
exit(EXIT_FAILURE);
}
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("connect error");
exit(EXIT_FAILURE);
}
signal(SIGINT, sigint_handler);
printf("Enter your name: ");
if (fgets(client_name, MAXLINE, stdin) == NULL) {
if (errno == EINTR) {
sigint_handler(0); // 如果输入名字时按 Ctrl+C,直接退出
}
else {
perror("fgets error");
close(sockfd);
exit(EXIT_FAILURE);
}
}
client_name[strcspn(client_name, "\n")] = '\0';
write(sockfd, client_name, strlen(client_name));
while (1) {
printf("Enter a string (Ctrl+C to exit): ");
if (fgets(buf, MAXLINE, stdin) == NULL) {
if (errno == EINTR) {
break; // 如果是 Ctrl+C 中断,直接退出循环
}
perror("fgets error");
break;
}
buf[strcspn(buf, "\n")] = '\0';
write(sockfd, buf, strlen(buf));
n = read(sockfd, buf, MAXLINE);
if (n <= 0) {
printf("Server disconnected\n");
break;
}
buf[n] = '\0';
printf("Reversed string: %s\n", buf);
}
close(sockfd);
return 0;
}
一些代码
#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_CLIENTS 10
int client_sockets[MAX_CLIENTS];
int client_count = 0;
void handle_client(int connfd) {
char buffer[BUFFER_SIZE];
ssize_t n;
while ((n = read(connfd, buffer, BUFFER_SIZE - 1)) > 0) {
buffer[n] = '\0';
printf("收到客户端消息: %s", buffer);
if (write(connfd, buffer, n) < 0) {
perror("write 失败");
break;
}
}
close( connfd );
printf("客户端连接已关闭\n");
}
int main() {
int listenfd, connfd;
fd_set read_fds;
int max_fd;
listenfd = socket(…);
/*略,假定TCP服务器已建立*/
printf("服务器已启动,监听端口 %d...\n", PORT);
// 初始化客户端套接字数组
for (int i = 0; i < MAX_CLIENTS; i++) {
client_sockets[i] = -1;
}
while (1) {
FD_ZERO (&read_fds); // 初始化文件描述符集合
FD_SET(listenfd, &read_fds);
max_fd = listenfd;
// 将客户端套接字添加到集合中
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] > 0) {
FD_SET (client_sockets[i], &read_fds); // 监控监听套接字
if (client_sockets[i] > max_fd) {
max_fd = client_sockets[i];
}
}
}
// 设置 select 的超时时间为 1 秒
struct timeval timeout;
timeout.tv_sec = 1 ;
timeout.tv_usec = 0;
// 调用 select 监控文件描述符
int activity = select( max_fd+1 , &read_fds, NULL, NULL, &timeout);
if (activity < 0) {
perror("select 失败");
break;
}
// 检查监听套接字是否有新连接
if ( FD_ISSET (listenfd, &read_fds)) {
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
if (connfd < 0) {
perror("accept 失败");
continue;
}
printf("新客户端连接: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 将新连接添加到客户端套接字数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == -1) {
client_sockets[i] = connfd;
client_count++;
break;
}
}
}
// 检查客户端套接字是否有数据可读
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] > 0 && FD_ISSET(client_sockets[i], &read_fds)) {
pid_t pid = fork() ; //创建子进程
if (pid == 0) {
close( listenfd ); //关闭套接字
handle_client(client_sockets[i]);
exit(0);
} else if (pid > 0) {
close(client_sockets[i]);
client_sockets[i] = -1;
client_count--;
} else {
perror("失败");
}
}
}
// 非阻塞回收僵尸进程
while (1) {
pid_t pid = waitpid (-1, NULL, WNOHANG);
if (pid <= 0) {
break; // 无子进程退出
}
printf("父进程回收子进程 PID=%d\n", pid);
}
}
close( listenfd ); // 关闭套接字
printf("服务器已关闭\n");
return 0;
}