Linux网络编程-回声服务器实现

目录

网络编程

文章目录

前言

一、网络的基本概念

1.1 网络的物理结构

 1.2 网络中的地址

 1.3 网络中的端口

 1.4 什么是协议?

1.5 TCP协议基础

二、套接字介绍

2.1 什么是套接字?

2.2 套接字的创建(socket函数)

2.3 bind函数 

 2.4 listen函数 

 2.5 accept函数 

2.6 TCP套件字的I/O缓冲

 三、TCP编程

3.1 TCP服务端

 3.2 服务端代码实现

3.3 服务端为什么要进行端口复用?

3.4 客户端代码实现

ps:inet_pton()函数

3.5 Connect()函数 


前言

主要用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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

康康今天学习了么

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值