目录
前言
主要用c语言写一个服务器,客户端不断向服务器发送1000字节的数据,服务器收到后对该数据进行取反,然后再发送给客户端,客户端判断该取反数据和原始发送数据是否一致。完成这个小demo以后,准备把从头到尾接触的知识全部记录下来。
一、网络的基本概念
1.1 网络的物理结构
数据如何从一台机器传送到另一台机器上面?首先介绍几个物理结构
主板上的网口:
网线和水晶头:
一块独立的网卡:
中断接口→操作系统→网络数据接收接口就会有数据
常见的网络连接方案:
数据→系统(OS)→网卡→路由器→modem(光猫盒)→互联网→modem(光猫盒)→路由器→网卡→系统(OS)→接收程序
1.2 网络中的地址
并不是所有的地址都是可以用的
32位网络地址由四个字节构成
255*255*255*255
1、 以0开头的地址,都是不可以用的
2 、以0结尾的地址,表示的是网段,而不是具体的地址
3 、224开头到239开头的地址,是组播地址,不可用于点对点的传输
4 、240开头到255开头的地址,是实验用的,保留,一般不做服务器或者终端地址
127.0.0.1保留的 回环网络的地址
0.0.0.0保留的,一般用于服务器监听的,表示全网段监听
组播可以理解为tcp/udp上面的广播,可以极大的节约带宽
问题:容易形成网络风暴
A类、B类、C类用于不同规模(大、中、小型)的网络
D类用于组播(类似广播)
E类保留,用于实验、私有网络
注意上述地址中缺少两个点,分别是0.0.0.0和127.0.0.X
0.0.0.0用于服务器监听的时候使用
意思是监听本机上所有的地址
大型服务器大部分都是有多个地址的
127.0.0.1是本机网络,往这个网络发任何数据,都会被回发回来
1.3 网络中的端口
公认端口(常用端口):这类端口的端口号从0到1024,它们紧密绑定于一些特定的服务。通常这些端口的通信明确表明了某种服务的协议,这种端口是不可再重新定义它的作用对象。 例如:80端口实际上总是HTTP通信所使用的,而23号端口则是Telnet服务专用的。这些端口通常不会像木马这样的黑客程序利用。
注册端口:端口号从1025~49151,它们松散地绑定于一些服务。也是说有许多服务绑定于这些端口,这些端口同样用于许多其他目的。这些端口多数没有明确的定义服务对象,不同程序可根据实际需要自己定义。
动态和/或私有端口(Dynamic and/or Private Ports):端口号从49152到65535(不要轻易作为服务器的监听端口)。
理论上,不应把常用服务分配在这些端口上。实际上,有些较为特殊的程序,特别是一些木马程序就非常喜欢用这些端口,因为这些端口常常不被引起注意,容易隐蔽。
端口并非只有服务器才会使用,客户端也一样会使用端口
1.4 什么是协议?
TCP协议包头说明:
HTTP协议包头样例(文本包头):
GET /search/detail?ct=503316480&z=0&ipn=d
HTTP/1.1
Host: pic.baidu.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3766.400 QQBrowser/10.6.4163.400
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
SSH数据包头结构:
协议就是一种网络式交互中数据格式和交互流程的约定
通过协议,我们可以与远程的设备进行数据交互
请求或者完成对方的服务
协议就是计算机中的语言
1.5 TCP协议基础
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议
可靠的协议
UDP协议 不可靠的协议
TCP协议是可靠的,但是牺牲了性能
基于字节流
可以发1个字节,也可以发1万个字节
数据是字节流
交互流程:
二、套接字介绍
2.1 什么是套接字?
网络编程就是编写程序使两台连网的计算机相互交换数据。两台计算机之间用什么传输数据呢?首先需要物理连接。在此基础上,只需考虑如何编写数据传输软件。但实际上这也不用愁,因为操作系统会提供名为"套接字"(socket)的部件。套接字是网络数据传输用的软件设备。即使对网络数据传输原理不太熟悉,我们也能通过套接字完成数据传输。因此,网络编程又称为套接字编程。那为什么要用"套接字"这个词呢?
套接字在英文里面就是“插孔/插座”的意思。
就如同我们要用电,要用到插孔,同样的,对于我们要用网络,那么就要用到socket。如此类比。
2.2 套接字的创建(socket函数)
套接字有很多种,其中用的最多的就是TCP和UDP;TCP套接字可以比喻成电话机。实际上,电话机也是通过固定电话网(telephone network)完成语音数据交换的。就如同于:创建一个套接字就相当于是安装了一部电话机。
int socket(int domain, int type, int protocol); 成功时返回文件描述符fd,失败时返回-1。
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed"); // 错误处理
exit(EXIT_FAILURE);
}
参数一:domain (Protocol Family)
头文件sys/socket.h中声明的协议族
在一般的网络编程程序里面,PF_INET对应的IPV4互联网协议是最常用的,我们目前只需要掌握这类协议族。
参数二:套接字类型:type
套接字类型指的是套接字的数据传输方式,通过socket函数的第二个参数传递,只有这样才能决定创建的套接字的数据传输方式。
那为什么已通过第一个参数传递了协议族信息,还要决定数据传输方式?问题就在于,决定了协议族并不能同时决定数据传输方式,换言之,socket函数第一个参数PF_INET协议族中也存在多种数据传输方式。
参数三:protocol 计算机间通信中使用的协议信息
大部分情况下可以向第三个参数传递0,
除非遇到以下这种情况∶
"同一协议族中存在多个数据传输方式相同的协议"
数据传输方式相同,但协议不同。此时需要通过第三个参数具体指定协议信息。
2.3 bind函数
调用bind函数给套接字分配地址后,就基本完成了接电话的所有准备工作。
如果把套接字比喻为电话,那么目前只安装了电话机。
接着就要给电话机分配号码的方法,即给套接字分配 IP地址和端口号。就是用的bind函数。
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
成功时返回0,失败时返回-1。
参数一:套接字描述符 sockfd
要分配地址信息(IP地址和端口号)套接字的文件描述符。
参数二:存有地址信息的结构体变量地址值 myaddr
地址信息的表示
struct sockaddr_in
{
sa_family//地址族(Address Family)
sa_family_t sin_family;// //地址族(Address Family)
uint16_t sin_port; // 16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
}
该结构体中提到的另一个结构体sin_addr定义如下,它用来存放32位IP地址。
struct in_addr
{
In_addr_t s_addr; //32位IPv4地址
};
成员sin_family --- 地址族(Address Family)
成员sin_port
该成员保存16位端口号
成员sin_addr
该成员保存32位IP地址信息,且也以网络字节序保存。为理解好该成员,应同时观察结构体in addr。但结构体in addr声明为uint32t,因此只需当作32位整数型即可。
成员sin_zero
无特殊含义。只是为使结构体sockaddr in的大小与sockaddr结构体保持一致而插入的成员。必需填充为0,否则无法得到想要的结果。
网络字节序与地址变换
大端序(Big Endian)∶高位字节存放到低位地址。
小端序(Little Endian)∶高位字节存放到高位地址。
字节序转换:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsined long ntohl(unsigned long);
通过函数名应该能掌握其功能,只需了解以下细节。
htons中的h代表主机(host)字节序。
htons中的n代表网络(network)字节序。
ntohs可以解释为"把short型数据从网络字节序转化为主机字节序"
struct sockaddr_in addr;
char* serv_ip="211.217.168.13"; //声明 IP地址字符串
char * serv_port="9198"; //声明端口号字符串
memset(&addr,e,sizeof(addr);//结构体变量addr的所有成员初始化为0
//指定地址族
addr.sin_family =AF_INET;
//基于字符串的IP地址初始化
addr.sin_addr.s_addr=inet_addr(serv_ip);
//基于字符串的端口号初始化
addr.sin_port=htons(atoi(serv_port));
//INADDRANY
//每次创建服务器端套接字都要输入IP地址会很繁琐,此时可初始化地址信息为INADDRANY。
addr.sin_addr.s_addr=htonl(INADDRANY);
参数三:第二个结构体变量的长度
一般直接用sizeof第二个参数就行
具体使用
address.sin_family = AF_INET; // 地址族(IPv4)
address.sin_addr.s_addr = INADDR_ANY; // 监听所有本地 IP 地址
address.sin_port = htons(PORT); // 端口号,网络字节序
// 绑定套接字到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed"); // 错误处理
exit(EXIT_FAILURE);
}
2.4 listen函数
int listen(int sock,int backlog);
→成功时返回0,失败时返回-1。
sock希望进入等待连接请求状态的套接字文件描述符
传递的描述符套接字参数成为服务器端 套接字(监听套接字)。
backlog 连接请求等待队列(Queue)的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列。
// 监听端口
if (listen(server_fd, 3) < 0) {
perror("listen"); // 错误处理
exit(EXIT_FAILURE);
}
监听等待连接的状态
由上图可知作为listen函数的第一个参数传递的文件描述符套接字的用途。
客户端连接请求本身也是从网络中接收到的一种数据,而要想接收就需要套接字。此任务就由服务器端套接字完成。服务器端套接字是接收连接请求的一名门卫或一扇门。
客户端如果向服务器端询问∶"请问我是否可以发起连接?"
服务器端套接字就会亲切应答∶"您好!当然可以,但系统正忙,请到等候室排号等待,准备好后会立即受理您的连接。"同时将连接请求请到等候室。
调用listen函数即可生成这种门卫(服务器端套接字)
listen函数的第二个参数决定了等候室的大小。
等候室称为连接请求等待队列,准备好服务器端套接字和连接请求等待队列后,这种可接收连接请求的状态称为等待连接请求状态。
2.5 accept函数
int accept(int sock,struct sockaddr * addr, socklen_t*addrlen);
成功时返回创建的套接字文件描述符,失败时返回-1。
参数sock:服务器套接字的文件描述符。
参数addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
参数addrlen:第二个参数结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填客户端地址长度。
// 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept"); // 错误处理
exit(EXIT_FAILURE);
}
调用listen函数后,若有新的连接请求,则应按序受理。如果在与客户端的数据交换中使用门卫,那谁来守门呢?因此需要另外一个套接字,但没必要亲自创建。此时accept应运而生。
accept函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。套接字是自动创建的,并自动与发起连接请求的客户端建立连接。上图展示了accept函数调用过程。
2.6 TCP套接字的I/O缓冲
IO缓冲在套接字的创建时自动生成。
我们知道,TCP套接字的数据收发无边界。服务器端即使调用1次write函数传输40字节的数据,客户端也有可能通过4次read函数调用每次读取10字节。但此处也有一些疑问,服务器端一次性传输了40字节,而客户端居然可以缓慢地分批接收。客户端接收10字节后,剩下的30字节在何处等候呢?是不是像飞机为等待着陆而在空中盘旋一样,剩下30字节也在网络中徘徊并等待接收呢?
实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确地说,如下图所示,write函数调用瞬间,数据将移至输出缓冲;read函数调用瞬间,从输人缓冲读取数据。
调用write函数时,数据将移到输出缓冲,在适当的时候(不管是分别传送还是一次性传送)传向对方的输入缓冲。这时对方将调用read函数从输入缓冲读取数据。这些I/O 缓冲特性可整理如下。
A: I/O缓冲在每个TCP套接字中单独存在。
B: I/O缓冲在创建套接字时自动生成。
C: 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
D: 关闭套接字将丢失输入缓冲中的数据。
那么,下面这种情况会引发什么事情?理解了I/O缓冲后,其流程∶
"客户端输入缓冲为50字节,而服务器端传输了100字节。"
这的确是个问题。输入缓冲只有50字节,却收到了100字节的数据。可以提出如下解决方案∶
填满输入缓冲前迅速调用read函数读取数据,这样会腾出一部分空间,问题就解决了。
其实根本不会发生这类问题,因为TCP会控制数据流。
TCP中有滑动窗口(Sliding Window)协议,用对话方式呈现如下。
套接字A∶"你好,最多可以向我传递50字节。"
套接字B∶"OK!"
套接字A∶"我腾出了20字节的空间,最多可以收70字节。
套接字B∶"OK!"
数据收发也是如此,因此TCP中不会因为缓冲溢出而丢失数据。
但是会因为缓冲而影响传输效率
三、TCP编程
3.1 TCP服务端
3.2 服务端代码实现
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库,包含内存分配、进程控制等
#include <string.h> // 字符串处理函数库
#include <unistd.h> // POSIX 操作系统 API 访问库
#include <arpa/inet.h> // 定义互联网操作的函数库
#define PORT 8080 // 服务器端口号
#define BUFFER_SIZE 1000 // 缓冲区大小
// 数据取反函数
void invert_data(char *data, size_t length) {
for (size_t i = 0; i < length; ++i) {
data[i] = ~data[i]; // 取反操作
}
}
int main() {
int server_fd, new_socket; // 服务器文件描述符和新连接的套接字
struct sockaddr_in address; // 存储地址信息
int opt = 1; // 套接字选项
int addrlen = sizeof(address); // 地址长度
char buffer[BUFFER_SIZE] = {0}; // 接收缓冲区
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed"); // 错误处理
exit(EXIT_FAILURE);
}
// 设置端口复用选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt"); // 错误处理
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET; // 地址族(IPv4)
address.sin_addr.s_addr = INADDR_ANY; // 监听所有本地 IP 地址
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);
}
while (1) {
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
int valread = read(new_socket, buffer, BUFFER_SIZE); // 读取客户端消息
if (valread <= 0) {
perror("read"); // 错误处理
break;
}
invert_data(buffer, valread); // 数据取反
send(new_socket, buffer, valread, 0); // 发送取反后的数据
}
}
3.3 服务端为什么要进行端口复用?
学习SO_REUSEADDR、SOREUSEPORT可选项之前,应理解好Timewait状态。
如果客户端先通知服务器端终止程序。在客户端控制台调用close函数,向服务器端发送FIN消息并经过四次挥手过程。当然,输人CTRL+C 时也会向服务器传递FIN消息。强制终止程序时,由操作系统关闭文件及套接字,此过程相当于调用close函数,也会向服务器端传递FIN消息。
"但看不到什么特殊现象啊?"
是的,通常都是由客户端先请求断开连接,所以不会发生特别的事情。重新运行服务器端也不成问题,但按照如下方式终止程序时则不同。
"服务器端和客户端已建立连接的状态下,向服务器端控制台输入CTRL+C,即强制关闭服务器端。"
这主要模拟了服务器端向客户端发送FIN消息的情景。但如果以这种方式终止程序,那服务器端重新运行时将产生问题。如果用同一端口号重新运行服务器端,将输出"bind)error"消息,并且无法再次运行。但在这种情况下,再过大约3分钟即可重新运行服务器端。
上述2种运行方式唯一的区别就是谁先传输FIN消息,但结果却迥然不同,原因何在呢?
假设上图中主机A是服务器端,因为是主机A向B发送FIN消息,故可以想象成服务器端1在控制台输入CTRL+C。但问题是,套接字经过四次挥手过程后并非立即消除,而是要经过一段时间的Time-wait状态。当然,只有先断开连接的(先发送FIN消息的)主机才经过Time-wait状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在Time-wait过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind函数调用过程中当然会发生错误。
有些人会误以为 Time-wait 过程只存在于服务器端。但实际上,不管是服务器端还是客户端,套接字都会有 Time-wait 过程。先断开连接的套接字必然会经过Time-wait 过程。但无需考虑客户端 Time-wait状态。因为客户端套接字的端口号是任意指定的。与服务器端不同,客户端每次运行程序时都会动态分配端口号,因此无需过多关注Time-wait状态。
解决方案就是在套接字的可选项中更改SO_REUSEADDR的状态。适当调整该参数,可将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR的默认值为0(假),这就意味着无法分配Time-wait状态下的套接字端口号。因此需要将这个值改成1(真)。//opt = 1;
3.4 客户端代码实现
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库,包含内存分配、进程控制等
#include <string.h> // 字符串处理函数库
#include <unistd.h> // POSIX 操作系统 API 访问库
#include <arpa/inet.h> // 定义互联网操作的函数库
#define PORT 8080 // 服务器端口号
#define BUFFER_SIZE 1000 // 缓冲区大小
// 生成数据函数
void generate_data(char *data, size_t length) {
for (size_t i = 0; i < length; ++i) {
data[i] = (char)i; // 填充数据
}
}
// 数据取反函数
void invert_data(char *data, size_t length) {
for (size_t i = 0; i < length; ++i) {
data[i] = ~data[i]; // 取反操作
}
}
int main() {
int sock = 0; // 套接字描述符
struct sockaddr_in serv_addr; // 服务器地址信息
char buffer[BUFFER_SIZE] = {0}; // 接收缓冲区
char original_data[BUFFER_SIZE] = {0}; // 原始数据缓冲区
// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET; // 地址族(IPv4)
serv_addr.sin_port = htons(PORT); // 端口号,网络字节序
// 将地址转换为二进制形式
if (inet_pton(AF_INET, "192.168.222.189", &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;
}
while (1) {
generate_data(original_data, BUFFER_SIZE); // 生成数据
send(sock, original_data, BUFFER_SIZE, 0); // 发送数据
int valread = read(sock, buffer, BUFFER_SIZE); // 读取服务器响应
if (valread <= 0) {
perror("read"); // 错误处理
break;
}
invert_data(buffer, BUFFER_SIZE); // 数据取反
// 检查数据是否一致
if (memcmp(original_data, buffer, BUFFER_SIZE) != 0) {
printf("Mismatch detected!\n");
} else {
printf("Data match.\n");
}
usleep(100000); // 延迟以避免过高的CPU使用率
}
close(sock); // 关
}
ps:inet_pton()函数
该函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。
功能:
将标准文本表示形式的IPv4或IPv6 Internet网络地址转换为数字二进制形式。
INT WSAAPI inet_pton(
INT Family, //地址家族 IPV4使用AF_INET IPV6使用AF_INET6
PCSTR pszAddrString, //指向以NULL为结尾的字符串指针,该字符串包含要转换为数字的二进制形式的IP地址文本形式。
PVOID pAddrBuf//指向存储二进制表达式的缓冲区
);
返回值:
1.若无错误发生,则inet_pton()返回1,pAddrBuf参数执行的缓冲区包含按网络字节顺序的二进制数字IP地址。
2.若pAddrBuf指向的字符串不是一个有效的IPv4点分十进制字符串或者不是一个有效的IPv6点分十进制字符串,则返回0。否则返回-1。
3.5 Connect()函数
int connect(int sock,struct sockaddr*servaddr, socklen_t addrlen);
成功时返回0,失败时返回-1。
● sock 客户端套接字文件描述符。
● servaddr 保存目标服务器端地址信息的变量地址值。
●addrlen 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度。
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
客户端套接字地址信息在哪?
实现服务器端必经过程之一就是给套接字分配IP和端口号。但客户端实现过程中并未出现套接字地址分配,而是创建套接字后立即调用conect函数。难道客户端套接字无需分配IP和端口?
答案:当然不是!
网络数据交换必须分配IP和端口。既然如此,那客户端套接字何时、何地、如何分配地址呢?
何时? 调用connect函数时。
何地? 操作系统,更准确地说是在内核中。
如何? IP用计算机(主机)的IP,端口随机。
客户端的IP地址和端口在调用connect函数时自动分配,无需调用标记的bind函数进行分配。
这就是与服务端的不同。
3.6 ps:客户端服务器传一个结构体
服务器代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define PORT 8080
typedef struct{
int i;
double d;
float f;
} Data;
struct server_args{
int server_fd,new_socket,epoll_fd;
int epoll_cnt;
int result;
}ser_args;
struct sockaddr_in address;
struct epoll_event ev,events[MAX_EVENTS];
int addrlen = sizeof(address);
char buffer[sizeof (Data)];
void modify_data(Data *data){
data->i += 1;
data->d *= 2;
data->f += 0.5;
}
void create_socket(){
if((ser_args.server_fd = socket(AF_INET,SOCK_STREAM,0)) == 0){
perror("socket failed");
exit(EXIT_FAILURE);
}
}
void initialize_addr(){
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
}
void bind_socket(){
if(bind(ser_args.server_fd,(struct sockaddr*)&address,sizeof (address)) < 0){
perror("bind failed");
close(ser_args.server_fd);
exit(EXIT_FAILURE);
}
}
void listen_socket(){
if(listen(ser_args.server_fd,3) < 0){
perror("listen failed");
close(ser_args.server_fd);
exit(EXIT_FAILURE);
}
}
void create_epoll(){
ser_args.epoll_fd = epoll_create1(0);
if(ser_args.epoll_fd == -1){
perror("epoll_create1 failed");
close(ser_args.server_fd);
exit(EXIT_FAILURE);
}
}
void add_socket_epoll(){
ev.events = EPOLLIN;
ev.data.fd = ser_args.server_fd;
if(epoll_ctl(ser_args.epoll_fd,EPOLL_CTL_ADD,ser_args.server_fd,&ev) == -1){
perror("epoll_ctl failed");
close(ser_args.server_fd);
close(ser_args.epoll_fd);
exit(EXIT_FAILURE);
}
}
int epoll_waiting(){
ser_args.epoll_cnt = epoll_wait(ser_args.epoll_fd,events,MAX_EVENTS,-1);
if(ser_args.epoll_cnt == -1){
perror("epoll_wait failed");
return -1;
}
}
void conmunication_with_client(){
for(int n = 0; n < ser_args.epoll_cnt; n++) {
if(events[n].data.fd == ser_args.server_fd){
ser_args.new_socket = accept(ser_args.server_fd,(struct sockaddr*)&address,(socklen_t *)&addrlen);
if(ser_args.new_socket == -1) {
perror("accept failed");
continue;
}
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = ser_args.new_socket;
if(epoll_ctl(ser_args.epoll_fd,EPOLL_CTL_ADD,ser_args.new_socket,&ev) == -1){
perror("epoll_ctl failed");
close(ser_args.new_socket);
continue;
}
}else{
int client_fd = events[n].data.fd;
int bytes_read = read(client_fd,buffer,sizeof (Data));
if(bytes_read > 0) {
Data *data = (Data *)buffer;
modify_data(data);
send(client_fd,buffer,sizeof(Data),0);
}else if (bytes_read == 0) {
close(client_fd);
}else{
perror("read failed");
close(client_fd);
}
}
}
}
int main(){
create_socket();
initialize_addr();
bind_socket();
listen_socket();
create_epoll();
add_socket_epoll();
while(1){
if((ser_args.result = epoll_waiting()) == -1) break;
conmunication_with_client();
}
close(ser_args.server_fd);
close(ser_args.epoll_fd);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
typedef struct{
int i;
double d;
float f;
}Data;
int main(){
int sock = 0;
struct sockaddr_in serv_addr;
Data data = {1,2.0,3.0};
Data received_data;
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(PORT);
if(inet_pton(AF_INET,"192.168.222.189",&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;
}
while(1){
send(sock,&data,sizeof(Data),0);
read(sock,&received_data,sizeof(Data));
printf("Client0: Received modified data: i = %d,d = %.2f,f = %.2f\n",received_data.i,received_data.d,received_data.f);
sleep(1);
}
close(sock);
return 0;
}