linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现

1 TCP简介

tcp是一种基于流的应用层协议,其“可靠的数据传输”实现的原理就是,“拥塞控制”的滑动窗口机制,该机制包含的算法主要有“慢启动”,“拥塞避免”,“快速重传”。

2 TCP socket建立和epoll监听实现

数据结构设计

linux环境下,应用层TCP消息体定义如下:

typedef struct TcpMsg_s
{
     TcpMsgHeader head;
     void* msg;
}TcpMsg;

其中,head表示自定义的TCP消息头,它的定义如下:

//TCP消息类型,根据业务需求定义typedef enum MSGTYPE _e
{
     EP_REG_REQ = 0,
     EP_REQ_RSP = 1,
}MSGTYPE;
//TCP消息头定义的通用框架
typedef struct TcpMsgHead_s
{
     int len;//消息长度(用作TCP粘包处理)
     MSGTYPE type;//消息类型(用作接收端消息的解析)
}TcpMsgHead;

socket建立C代码

TCP客户端和服务端都采用linux提供的epoll机制(epoll_create(),epoll_wait(),epoll_ctl())对socket实现监听(可读,可写事件等)。

开源事件驱动库lievent对socket事件的监听也是通过对epoll事件的封装实现的。

(1)TCP服务端socket建立C代码

基本原理:利用linux网络通信API(scoket(),bind(),listen())来创建服务器端socket;

代码如下:输入参数:localip,本地ip;port:服务端本地的监听端口号;输出:返回-1,表示失败;返回>0的fd,表示socket建立成功;

 1    int TcpServer(uint32_t lcoalip, int port)
 2    {
 3        int fd;
 4        struct sockaddr_in addr;
 5
 6        //socket建立
 7        if ((fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
 8        {
 9            printf("IN TcpServer() scoket created failed,errno is %d, strerror is %s\n", errno, strerror(errno));
10            return -1;
11        }
12
13        //设置socket为非阻塞模式
14        int flags = fcntl(fd, F_GETFL, 0);
15        fcntl(fd, F_SETFL, flags | O_NONBLOCK);
16
17        memset(&addr, 0 , sizeof(addr));
18        addr.sin_family = AF_INET;
19        addr.sin_addr.s_addr = localip;
20        addr.sin_port = port;
21
22        //绑定本地端口和IP
23        if (bind(fd, (struct sockaddr_in)&addr, sizeof(addr) < 0))
24        {
25            printf("IN TcpServer() bind failed,fd is%d, errno is %d, strerror is %s\n", fd, errno, strerror(errno));
26            return -1;
27        }
28
29        if (listen(fd, 20< 0))
30        {
31            printf("IN TcpServer() listen failed,fd is%d, errno is %d, strerror is %s\n", fd, errno, strerror(errno));
32            return -1;
33        }
34
35        //add the socket to epoll event
36        if (SubscribeFd(fd, SOCKET_EV) != 0)          {               return -1;          }
37        return fd;
38   }            

而SubscribeFd函数功能是将socket添加到epoll的监听事件中

实现如下:

输入参数:fd,待监听的fd;type,枚举型变量,表明TCP类型,是客户端还是服务端;port:服务端的监听端口号;输出:返回-1,表示监听失败;返回0,表示将该socket成功添加到维护在全局变量g_epoll(TCP_EPOLL类型结构体)中的监听事件中;其中TCP_TYPE枚举变量和TCP_EPOLL结构体的定义如下:

typedef enum
{
     CLIENT = 0,
     SERVER = 1,
}TCP_TYPE;

#define MAX_NUM_EPOLL 1000//最多可监听的socket数目
typedef struct TCP_EPOLL_s
{
     struct epoll_event* p_event;
     int nb_evnet;     int nb_client;//for tcp server
     int epoll_fd;
     int sock_listen;//for tcp server
     int sock[MAX_NUM_EPOLL];
     TCP_NL_MSG* p_tcp_nl_msg;//TCP粘包处理数据结构
}TCP_EPOLL;

SubscribeFd函数实现如下:

int SubscribeFd (int fd, TCP_TYPE type)
{
    struct epoll_event event;

    if (CLIENT == type)
    {
        event.events = EPOLLOUT | EPOLLET;//监听类型为可写事件
    }
    else if (SERVER == type)
    {
        event.events = EPOLLIN | EPOLLET;//监听类型为可读事件
    }

    event.date.u64 = 0;
    evnet.data.fd = fd;

    g_epoll.nb_event++;
    g_epoll.p_event = realloc(g_epoll.p_event, g_epoll.nb_event * sizeof(struct epoll_event));

    //add epoll control event
    if (epoll_ctl(g_epoll.epoll_fd, EPOLL_CTL_ADD, fd, &event) != 0)
    {
        printf("epoll_ctl failed for fd %d, errno is %d, strerror is %s\n", fd, errno, strerror(errno));
        return -1;
    }

    printf("successfully subscribe fd %d\n", fd);

    return 0;
}

(2)TCP客户端socket建立C代码

基本原理:利用linux网络通信API(scoket(),connect())来创建客户端socket;

代码如下:输入参数:peerip,服务端IP;localip,本地ip;port:服务端的监听端口号;输出:返回-1,表示失败;返回>0的fd,表示socket建立成功;

 1 int TCPClient(uint32_t peerip,uint32_t localip,uint16_t port)
 2 {
 3      int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 4      if (fd < 0)
 5      {
 6        printf("TCPClient() socket failed");
 7        return -1;
 8      }
 9
10     struct sockaddr_in localaddr = {0};
11     localaddr.sin_family = AF_INET;
12     localaddr.sin_addr.s_addr = localip;
13     //localaddr.sin_port = htons(port);
14
15     int ret = bind(fd, (struct sockaddr *)&localaddr, sizeof(localaddr));
16     if (ret < 0)
17     {
18         printf("TCPClient() bind failed localip %u", localip);
19         return -1;
20     }
21
22     int flags = fcntl(fd, F_GETFL, 0);
23     fcntl(fd, F_SETFL, flags | O_NONBLOCK);
24
25     struct sockaddr_in servaddr = {0};
26     servaddr.sin_family = AF_INET;
27     servaddr.sin_addr.s_addr = peerip;
28     servaddr.sin_port = htons(port);
29
30     ret = connect(fd, (sockaddr *)&servaddr, sizeof(servaddr));
31     if(ret < 0)
32     {
33         if (errno != EINPROGRESS)
34         {
35             printf("TCPClient() connect failed, peerip %u, port %u", peerip, port);
36             return -1;
37         }
38     }
39
40     printf("TCPClient() connect success, fd = %u,peerip %u, port %u",fd, peerip, port);
41
42     return fd;
43 }

(3) TCP客户端和服务端利用epoll_wait()实现对socket的监听和消息的接收的通用框架

TCP服务端监听到监听socket有EPOLLIN事件到来时,调用int accept_fd = accept();接收此连接请求,然后服务端要利用epoll_create()为accept_fd创建新的监听事件;

linux利用epoll机制实现socket事件的消息接收的C代码(TCP接收线程的入口)如下:

 1 void tcp_thread()
 2 {
 3     CreateEpoll();
 4     CreateSocketFdEpoll(g_tcp_type);
 5
 6     while (1)
 7     {
 8         //wait for a message
 9         EpollRecvMsg();
10     }
11 }

CreateEpoll函数是调用epoll_create来创建epoll事件:

1 TCP_EPOLL g_epoll;//全局Epoll变量
2
3 //EPOLL事件的建立
4 void CreateEpoll()
5 {
6      g_epoll.epoll_fd = epoll_create1(0);
7      g_epoll.nb_event = 0;
8 }

CreateSocketFdEpoll函数功能为创建TCP socket和TCP粘连处理数据结构初始化:

 1 int CreateSocketFdEpoll(TCO_TYPE type)
 2 {
 3     uint32_t server_ip = inet_addr(SERVER_IP);
 4     uint32_t local_ip = inet_addr(LOCAL_IP);
 5
 6     int fd;
 7     if (CLIENT == type)
 8     {
 9         fd = TcpClient(server_ip, SERVER_PORT, local_ip);
10         g_epoll.sock = fd;
11     }
12     else if (SERVER == type)
13     {
14         fd = TcpServer(local_ip, LOCAL_PORT);
15         g_epoll.sock_listen = fd;
16     }
17
18     g_epoll.p_tcpNLMsg = (TCP_NL_MSG)malloc(sizeof(TCP_NL_MSG));
19
20     InitTcpNLMsg(g_epoll.p_tcpNLMsg);
21 }

InitTcpNLMsg函数是对TCP粘连处理数据结构的初始化:

1 void InitTcpNLMsg(TCP_NL_MSG* pTcpNLMsg)
2 {
3     pTcpNLMsg->g_recv_len = 0;
4     pTcpNLMsg->flag_in_NL_proc = FALSE;
5     memset(pTcpNLMsg->g_recv_buff, 0, MAX_MSG_LEN);
6 }

EpollRecvMsg函数是调用epoll_wait()实现对Socket事件的监听和消息的接收:

 1 void EpollRecvMsg()
 2 {
 3     int epoll_ret = 0;
 4     int epoll_timeout = -1;
 5
 6     do
 7     {
 8         epoll_ret = epoll_wait(g_epoll.epoll_fd, g_epoll.p_event, g_epoll.nb_event, epoll_timeout);
 9     }while(epoll_ret < 0 && errno == EINTR);
10
11     if (epoll_ret < 0)
12     {
13         printf("epoll_wait failed: %s\n", strerror(errno));
14         return;
15     }
16
17     //遍历处理每一个当前监听到的事件
18     for (int i=0;i<epoll_ret;++i)
19     {
20          int fd = g_epoll.p_event[i].data.fd;
21
22          if (CLIENT == g_tcp_type)
23          {
24              if (g_epoll.p_event[i].events & EPOLLOUT) //the socket is writable,socket可写,表明服务端已accept该客户端的connect请求
25             {
26                  if (JudgeIfConnSucc(fd) == 0)//判断TCP连接是否建立成功
27                  {
28                       struct epoll_event* p_ev = &(g_epoll.p_event[i]);
29                       p_ev ->events = EPOLLIN | EPOLLET;
30
31                       epoll_ctl(g_epoll.epoll_fd, EPOLL_CTL_MOD,fd, p_ev );//对TCP客户端socket修改其监听类型,由可写改为可读
32
33                       printf("tcp_fd_client %d can be written\n", fd);
34                  }
35             }
36             else if(g_epoll.p_event[i].events & EPOLLIN) //the socket is readable
37             {
38                  RecvTcpMsg(fd);
39             }
40          }
41          else if (SERVER== g_tcp_type)
42          {  if (g_epoll.p_event[i].events & EPOLLIN) //the socket is readable,服务端socket可读
43             {
44                  if (fd == g_epoll.sock_listen)//服务端接收到一个TCP连接请求
45                  {
46                       struct sockaddr s_addr;
47                       socklen_t length = sizeof(struct sockaddr);
48
49                       int conn_fd = accept(fd, &s_addr, &length);//服务端接收来自客户端的连接请求
50
51                       int flags = fcntl(conn_fd, F_GETFL, 0);
52                       fcmt(conn_fd, F_SETFL, flags | O_NONBLOCK);
53
54                       g_epoll.sock[g_epoll.nb_client++] = conn_fd;
55
56                       SubscribeFd(conn_fd, SERVER);//服务端将新建立的TCP连接建立新的epoll监听事件,并维护在全局变量中
57
58                       printf("Receive a tcp conn request, conn_fd is %d\n", fd);
59                  }
60                 else //support multi tcp client
61                 {
62                      RecvTcpMsg(fd);//接收TCP消息(先进行粘包处理,然后根据消息类型进入不同的处理分支)
63                 }
64             }
65          }
66     }
67 }                            

(4)通用的TCP消息发送函数

函数实现如下:

输入:fd,发送socket;type,业务定义的tcp消息类型;msg指针:指向待发送的消息地址;length,待发送的msg的字节数;

输出:成功,返回发送的字节数;失败,返回-1;

#define MAX_LEN_BUFF 65535int SendTcpMsg(int fd, MSGTYPE type, void* msg, int length)
{
    uint8_t buf[MAX_LEN_BUFF];
    memset(buf,0,MAX_LEN_BUFF);
    uint32_t bsize = 0;

    TcpMsgHead* head = (TcpMsgHead*)buf;
    bsize += sizeof(TcpMsgHead);
    //将待发送消息内容拷贝到待发送缓存中
    memcpy(buf+bsize, msg, length);

    bsize += length;
    //封装TCP消息头,指明消息类型(用作接收端消息的解析)和消息长度(用作TCP粘包处理)
    head->type = type;
    head->msglen = bsize; 

    int ret = send(fd,(const void*)buf,bsize,0);
    if(ret != bsize)
    {
        printf("Failed to send tcp msg,errno=%u,ret=%d, strerror is %s\n", errno, ret, strerror(errno));
        return -1;
    }

    printf("Success to send tcp msg, msg type is %d\n", type); 

    return ret;

}
时间: 2024-12-28 15:45:04

linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现的相关文章

[Java]命令行模拟TCP客户端与服务端的简单小程序遇到的问题(基础不牢!扎实和亲手实践比什么都重要!)

简单得不能再简单的需求: 简单模拟TCP客户端与服务端的一次连接和通信,客户端发出一个消息,服务端回馈一个消息 自己第一次编写的代码: Client: class TcpClient1 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10010); OutputStream out=s.getOutputStream(); out.write(&quo

linux socket编程:简易客户端与服务端

什么是socket? socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作.其实socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO.打开.关闭) socket的作用是用于网络通讯,网络通讯一般指的是不同主机之间的进程通讯,比如我电脑上的qq和你电脑上的qq实现通讯,都是进程之间发送数据. 在本地用pid标识一个进

Linux中通过ssh将客户端与服务端的远程连接

前提需要:1.在VMware中装上两台linux虚拟机,本博客使用的都是CentOS 7.2.两部虚拟机可以通过命令ping通.3.两部虚拟机中已经通过yum本地仓库安装了sshd服务. 首先  1.执行命令 :yum install sshd 安装sshd服务         2.启动sshd服务 命令:systemctl start sshd         3.查看是否有22号端口因为以后要用到  命令:netstat -an|more 再来介绍一下我使用的虚拟机以及所配置的IP1. hb

使用socket实现简单的客户端和服务端通信(C#语言)

1.主要思路: (1) 服务端开启监听线程,等待客户端的连接. 每个socket连接放到独立线程中处理. (2) 服务端和客户端使用约定的消息格式通信.对于比较复杂的消息(如向服务端传递一个实例),可以使用json封装传输. (3) 每个连接的客户端,注册唯一的ClientID,在服务端以此来区分消息的来源. 2.代码构成 实现该样例包含两个cs的客户端程序. 分别为 服务端 和 客户端. 3.主要代码: (1) 服务端 xaml <Window x:Class="WpfApplicati

基于socket的客户端和服务端聊天机器人

服务端代码如下: using System;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;using System.Windows.Forms; namespace Client{ public partial class Form1 : Form { public Form1() { InitializeComponent(); //对 Windows 窗体控件进行线程安全调

带线程池的socket客户端与服务端

前言 socket(套接字),Socket和ServerSocket位于java.net包中,之前虽然对socket有过一些了解,但一直都是云里雾里的,特意仔细的学习了一个socket,用socket模拟一个天气查询的功能,并且解决了几个使用socket过程中比较严重的问题. 最简单的客户端和服务端 服务端代码 1 package cn.hucc.socket.server; 2 3 import java.io.DataInputStream; 4 import java.io.DataOut

QTcpSocket-Qt使用Tcp通讯实现服务端和客户端

版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:QTcpSocket-Qt使用Tcp通讯实现服务端和客户端     本文地址:http://techieliang.com/2017/12/530/ 文章目录 1. 基本功能  1.1. pro文件配置  1.2. QTcpServer服务端建立  1.3. 客户端建立  1.4. 消息收发 2. 其他  2.1. 实现单服务器多客户端通讯  2.2. 关于QTcpServer  2.3.

二、网络编程-socket之TCP协议开发客户端和服务端通信

知识点:之前讲的udp协议传输数据是不安全的,不可靠不稳定的,tcp协议传输数据安全可靠,因为它们的通讯机制是不一样的.udp是用户数据报传输,也就是直接丢一个数据包给另外一个程序,就好比寄信给别人,信丢了你也不知道,tcp传输需要先和服务端建立连接,当客户端与服务器连接时,服务器会给出应答,我俩连上了,而且数据传过来还会进行一个数据包数量验证,不一致会重新发送,还有其他种种验证,总之保证了数据传输安全可靠   这一章主要介绍使用套接字,编写一个tcp协议客户端和服务端.同样要用到上一章节提到小

linux 安装php时不安装mysql客户端或者服务端

php5.3以上就不要安装mysql 客户端,就可以使php支持mysql截取官网  “对于 php-5.3.0或更新版本,mysqli 默认使用Mysql Native Driver作为驱动. 这个驱动比libmysql会有一些优势, --with-mysql=mysqlnd  需要在./configure 时添加  --with-mysql=mysqlnd这个映射. ./configure --prefix=/usr/local/php --with-apxs2=/usr/local/apa