TCP服务端和客户端开发基本流程(简易聊天窗口)
本节通过学习TCP服务端和客户端开发基本流程,对libevent库官方sample进行改写,完成一个简易聊天窗口的小项目, 功能:server启动后,client端启动并连接,在client中输入文字,server端收到后打印出来,在server中输入文字,client也能接收并打印。
服务端开发流程
1、创建event_base_new()创建框架上下文对象event base 2、evconnlistener_new_bind分配一个监听器对象,监听给定地址上的TCP连接,通知设置监听回调(当新连接到来时,框架会调用)。 备注:这个函数相当于完成系统调用socket()、bind()、listen(),并设置accept回调函数 (1)在监听回调中通过一个已存在的socket描述符创建socket bufferevent; (2)设置bufferevent事件的回调函数; (3)启用/禁用bufferevent相关写缓存区 3、启动事件调度循环event_base_dispatch() 4、根据业务逻辑进行读写操作bufferevent_read() /bufferevent_write() 5、结束则释放相关资源;
客户端开发流程
1、创建event_base_new()创建框架上下文对象event base 2、使用bufferevent socket new()创建和服务器通讯的bufferevent事件 3、设置bufferevent事件的事件回调函数bufferevent_setcb(),在事件回调体中 (1)判断events事件值为BEV_EVENT_CONNECTED,则进行连接成功的逻辑处理; (2)判断events事件值为BEV_EVENT_ERROR,则进行连接发生错误的逻辑处理; 4、bufferevent_socket_connect()连接服务器 (1)若直接返回-1,则代表已经出错,进行出错逻辑处理; (2)如果返回0,则代表几种可能,可能成功,也可能还在握手阶段,具体根据设置的事件回调中进行判断 5、启动事件调度循环event_base_dispatch() 6、根据业务逻辑进行读写操作bufferevent_read() /bufferevent_write() 7、结束则释放相关资源;
服务端代码:
注意:我们要求能读取终端输入的消息并互相发送给对方,这里我最初的想法是在conn_readcb回调函数中,当我们的读取到消息后,再使用gets()函数获取键盘输入,然后bufferevent_write给服务器,但是这样的话程序会一直阻塞等着我输入,不能正常的再次调用conn_readcb回调函数去读取;所以我们的conn_readcb回调函数中是不能使用gets这种输入的。 后来到网上看了一种方式解决,通过使用一个事件来单独检测键盘输入,当键盘有输入时,触发该事件进行回调,然后在对应回调函数中进行bufferevent_write; 综上所述,服务器的代码大致流程: 1、新建一个event_base,然后在其上绑定一个listener来监听特定端口(9995); 2、新建一个处理信号的事件signal_event,并与上一步中的event_base绑定; 3、调用event_base_dispatch来监控两个事件,包括指定的TCP连接及SIGINT信号: ??当监听到一个连接时,libevent会触发listen_callback,即为上面的listener_cb函数,该函数完成: ??(1)、首先会创建一个基于底层的socket的bufferevent和一个用来监听终端输入的event_cmd;将event_cmd加入到event_base中进行监听终端输入; ??(2)、然后设置bufferevent为可写可读,调用bufferevent_write函数向其中缓存区写。bufferevent_write函数写后会触发写回调函数conn_writecb,由于该例子中不需要其他操作,所以conn_writecb函数直接不进行任何操作。 ??(3)、当终端有输入时,调用cmd_msg_cb回调函数read输入的消息,然后bufferevent_write将消息写入缓冲区; 4、当中断信号(Ctrl+C)出现时,libevent会触发signal_event的回调函数signal_cb,该函数会在2秒后停止event_base的监听,并退回到主函数; 5、主函数依次释放listener,signal_event和event_base的资源,结束。
补充:STDIN_FILENO STDIN_FILENO属于系统API接口库,其声明为 int 型,是一个打开文件句柄,对应的函数主要包括 open/read/write/close 等系统级调用。 操作系统一级提供的文件API都是以文件描述符来表示文件。STDIN_FILENO就是标准输入设备(一般是键盘)的文件描述符。
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#ifndef _WIN32
#include <netinet/in.h>
# ifdef _XOPEN_SOURCE_EXTENDED
# include <arpa/inet.h>
# endif
#include <sys/socket.h>
#endif
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/listener.h>
#include <event2/util.h>
#include <event2/event.h>
static char getClientMsg[1024]={0};
static char sendClientMsg[1024]={0};
static int sendcnt = 1;
static const int PORT = 9995;
int clientCnt = 0;
static void listener_cb(struct evconnlistener *, evutil_socket_t,
struct sockaddr *, int socklen, void *);
static void conn_readcb(struct bufferevent *, void *);
static void conn_writecb(struct bufferevent *, void *);
static void conn_eventcb(struct bufferevent *, short, void *);
static void signal_cb(evutil_socket_t, short, void *);
static void cmd_msg_cb(int , short , void* arg);
int
main(int argc, char **argv)
{
struct event_base *base; //定义event_base变量
struct evconnlistener *listener;//定义一个evconnlistener来监测端口(9995)
struct event *signal_event; //定义一个处理信号的事件来处理ctrl+c的信
号
struct sockaddr_in sin = {0}; //socket连接的协议地址结构体
#ifdef _WIN32
WSADATA wsa_data;
WSAStartup(0x0201, &wsa_data);
#endif
base = event_base_new();
if (!base) {
fprintf(stderr, "Could not initialize libevent!\n");
return 1;
}
//创建socket连接,设置sockaddr_in对应参数
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
listener = evconnlistener_new_bind(base, listener_cb, (void *)base,
LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1,
(struct sockaddr*)&sin,
sizeof(sin));
if (!listener) {
fprintf(stderr, "Could not create a listener!\n");
return 1;
}
signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base);
//判断signal_event是否创建成功,成功则并将其加入libevent事件链表上
if (!signal_event || event_add(signal_event, NULL)<0) {
fprintf(stderr, "Could not create/add a signal event!\n");
return 1;
}
//进入主循环,检测事件是否发生,事件发生调用对应回调函数
event_base_dispatch(base);//实际上是调用了event_base_loop();
//依次释放listener,signal_event和event_base的资源,结束
evconnlistener_free(listener);
event_free(signal_event);
event_base_free(base);
printf("done\n");
return 0;
}
static void
listener_cb(struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *sa, int socklen, void *user_data)
{
struct event_base *base = user_data;
struct bufferevent *bev;
struct event *ev_cmd;
bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
if (!bev) {
fprintf(stderr, "Error constructing bufferevent!");
event_base_loopbreak(base);//结束事件主循环
return;
}
ev_cmd = event_new(base, STDIN_FILENO,
EV_READ | EV_PERSIST, cmd_msg_cb,
(void*)bev);
event_add(ev_cmd, NULL);
bufferevent_setcb(bev, conn_readcb, conn_writecb, conn_eventcb, NULL);
bufferevent_enable(bev, EV_WRITE);
bufferevent_enable(bev, EV_READ);
char Msg[64] = {0};
sprintf(Msg, "welcome NO.%d client ", ++clientCnt);
bufferevent_write(bev, Msg, strlen(Msg));
}
static void cmd_msg_cb(int fd, short events, void* arg)
{
char msg[1024]={0};
int ret = read(fd, msg, sizeof(msg));
if( ret < 0 )
{
perror("read fail ");
exit(1);
}
struct bufferevent* bev = (struct bufferevent*)arg;
//把终端的消息发送给服务器端
bufferevent_write(bev, msg, ret);
}
static void
conn_readcb(struct bufferevent *bev, void *user_data)
{
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
memset(getClientMsg,0,1024);
bufferevent_read(bev, getClientMsg, len);
getClientMsg[len] = '\0';
printf("recv from NO.%d client =======>%s\n",clientCnt, getClientMsg);
}
static void
conn_writecb(struct bufferevent *bev, void *user_data)
{
}
static void
conn_eventcb(struct bufferevent *bev, short events, void *user_data)
{
//BEV_EVENT_EOF遇到文件结束指示
if (events & BEV_EVENT_EOF) {
printf("Connection closed.\n");
}
//BEV_EVENT_ERROR操作时发生错误
else if (events & BEV_EVENT_ERROR) {
printf("Got an error on the connection: %s\n",
strerror(errno));/*XXX win32*/
}
/* None of the other events can happen here, since we haven't enabled
* timeouts */
bufferevent_free(bev);
}
static void
signal_cb(evutil_socket_t sig, short events, void *user_data)
{
struct event_base *base = user_data;
struct timeval delay = { 2, 0 };//定义delay为2s
printf("Caught an interrupt signal; exiting cleanly in two seconds.\n");
event_base_loopexit(base, &delay);//2s后结束事件主循环
}
客户端代码
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
static char getServerMsg[1024]={0};
static char sendServerMsg[1024]={0};
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
if (events & BEV_EVENT_CONNECTED) {
printf("connect success\n");
const char* Msg = "hello, server";
bufferevent_write(bev, Msg, strlen(Msg));
return;
} else if (events & BEV_EVENT_ERROR) {
/* An error occured while connecting. */
printf("An error occured while connecting\n");
}
bufferevent_free(bev);
}
static void
conn_readcb(struct bufferevent *bev, void *user_data)
{
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
memset(getServerMsg,0,1024);
bufferevent_read(bev, getServerMsg, len);
getServerMsg[len] = '\0';
printf("recv from server =======> %s \n", getServerMsg);
static void
conn_writecb(struct bufferevent *bev, void *user_data)
{
/*
memset(sendServerMsg,0,1024);
printf("ser:>>");
gets(sendServerMsg);
//scanf("%s", g_szWriteMsg);
bufferevent_write(bev, sendServerMsg, strlen(sendServerMsg));
*/
}
static void cmd_msg_cb(int fd, short events, void* arg)
{
char msg[1024]={0};
int ret = read(fd, msg, sizeof(msg));
if( ret < 0 )
{
perror("read fail ");
exit(1);
}
struct bufferevent* bev = (struct bufferevent*)arg;
//把终端的消息发送给服务器端
bufferevent_write(bev, msg, ret);
}
int main(int argc,char **argv)
{
if(argc!=3){
printf("请输入正确的ip地址和端口号\n");
exit(0);
}
struct event_base *base;
struct bufferevent *bev;
struct event *ev_cmd;
struct sockaddr_in sin;
base = event_base_new();
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
inet_aton(argv[1],&sin.sin_addr); /* 127.0.0.1 */
sin.sin_port = htons(atoi(argv[2])); /* Port 9995 */
bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
//使用一个事件来检测键盘输入STDIN_FILENO为标准1输入设备(键盘)的文件描
述符
ev_cmd = event_new(base, STDIN_FILENO,
EV_READ | EV_PERSIST, cmd_msg_cb,
(void*)bev);
event_add(ev_cmd, NULL);
bufferevent_setcb(bev, conn_readcb,NULL, eventcb, NULL);
if (bufferevent_socket_connect(bev,
(struct sockaddr *)&sin, sizeof(sin)) < 0) {
/* Error starting connection */
bufferevent_free(bev);
return -1;
}
bufferevent_enable(bev, EV_WRITE|EV_PERSIST);
bufferevent_enable(bev, EV_READ|EV_PERSIST);
event_base_dispatch(base);
bufferevent_free(bev);
printf("finished\n");
return 0;
}
运行结果
服务器:
客户端:
多个客户端连接也是没有问题的。
|