之前使用IPC编写过聊天程序,但是这样仅能在同一台计算机上进行聊天;要使得在不同的计算机(不同的IP+端口)上也能进行通信,就需要用到socket编程。前面说到,要处理多客户端的响应问题,需要I/O复用,即调用select或者epoll。通常我们使用epoll函数,以下例子也是。
接下来,我们需要封装一个地址类。为什么要封装这样一个类呢?
在前面的练习中,我们可以看到,在socket规程中,需要反复用到struct sockaddr_in 这个地址,包括以下的bind绑定过程也是经常出现的,而且这些方法其实都是系统函数,我们并不希望每次都直接使用,不进繁琐难记,而且可读性差。所以,我们需要封装一个地址类CAdress,将这些步骤在函数内部实现。
编写CAdress类
CAdress.h:
#ifndef _ADRESS_H_
#define _ADRESS_H_
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h>
class CAdress
{
public:
CAdress(char *ip,unsigned short port);
CAdress();
~CAdress();
void setIP(char *ip);
void setPort(unsigned short port);
char *getIP();
unsigned short getPort();
struct sockaddr *getAddr();
socklen_t getAddrLen();
socklen_t *getAddrLenPtr();
private:
struct sockaddr_in m_addr;
socklen_t m_addrLen;
};
#endif
CAdress.cpp:
#include "adress.h"
CAdress::CAdress( char *ip,unsigned short port )
{
m_addr.sin_family = AF_INET;
m_addr.sin_port = htons(port);
m_addr.sin_addr.s_addr = inet_addr(ip);
m_addrLen = sizeof(struct sockaddr_in);
}
CAdress::CAdress()
{
m_addrLen = sizeof(struct sockaddr_in);
}
CAdress::~CAdress()
{
}
void CAdress::setIP( char *ip )
{
m_addr.sin_addr.s_addr = inet_addr(ip);
}
void CAdress::setPort( unsigned short port )
{
m_addr.sin_port = htons(port);
}
char * CAdress::getIP()
{
return inet_ntoa(m_addr.sin_addr);
}
unsigned short CAdress::getPort()
{
return ntohs(m_addr.sin_port);
}
struct sockaddr * CAdress::getAddr()
{
return (struct sockaddr *)&m_addr;
}
socklen_t CAdress::getAddrLen()
{
return m_addrLen ;
}
socklen_t *CAdress::getAddrLenPtr()
{
return &m_addrLen ;
}
编写主程序
common.h:
#include <string.h>
#include <signal.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <list>
#include <stdlib.h>
#include <map>
#include <string>
#include <signal.h>
#include <iostream>
#include "adress.h"
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h>
using namespace std;
//包头
typedef struct pack_head
{
char type; //1-登录包 2-聊天包
int size; //包体的长度
}PK_HEAD;
//登录包
typedef struct pack_login
{
char name[10];
char pwd[8];
}PK_LOGIN;
//消息包
typedef struct pack_chat
{
char fromName[10];
char toName[10];
char msg[100];
}PK_CHAT;
#define HEAD_SIZE sizeof(PK_HEAD)
#define LOGIN_SIZE sizeof(PK_LOGIN)
#define CHAT_SIZE sizeof(PK_CHAT)
#define LOGIN_OK 1
#define LOGIN_FAIL 0
#endif
server.cpp:
#include "common.h"
#define MAX_LISTEN_SIZE 10
#define MAX_EPOLL_SIZE 1000
#define MAX_EVENTS 20
int main()
{
int sockfd;
int connfd;
int reuse = 0;
int epfd;
int nEvent = 0;
struct epoll_event event = {0};
struct epoll_event rtlEvents[MAX_EVENTS] = {0};
char acbuf[100] = "";
int ret;
PK_HEAD head = {0}; //包头
PK_LOGIN login ={0}; //登录包
PK_CHAT chat = {0}; //聊天包
map<string,int> userMap; //<文件描述符,用户名>
map<string,int>::iterator it;
int reply; //登录应答包。 1-成功 0-失败
//1.socket()
sockfd = socket(PF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
return -1;
}
//2.bind()
char ip[20] = "192.168.159.6";
CAdress addr(ip,1234);
ret = bind(sockfd,addr.getAddr(),addr.getAddrLen());
if(ret == -1)
{
perror("bind");
return -1;
}
//3.listen()
ret = listen(sockfd,MAX_LISTEN_SIZE);
if(ret == -1)
{
perror("listen");
return -1;
}
//4.epoll初始化
epfd = epoll_create(MAX_EPOLL_SIZE); //创建
event.data.fd = sockfd;
event.events = EPOLLIN ;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event); //添加sockfd
CAdress connAddr;
//5.通信
while(1)
{
nEvent = epoll_wait(epfd,rtlEvents,MAX_EVENTS,-1); //阻塞
if(nEvent == -1)
{
perror("epoll_wait");
return -1;
}
else if(nEvent == 0)
{
printf("time out.");
}
else
{
//有事件发生,立即处理
for(int i = 0; i < nEvent; i++)
{
//如果是 sockfd
if( rtlEvents[i].data.fd == sockfd )
{
connfd = accept(sockfd,connAddr.getAddr(),connAddr.getAddrLenPtr());
//添加到事件集合
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&event);
printf("client ip:%s ,port:%u connect.\n",connAddr.getIP(),connAddr.getPort());
}
else //否则 connfd
{
ret = read(rtlEvents[i].data.fd,acbuf,100);
if( ret == 0) //客户端退出
{
close(rtlEvents[i].data.fd);
//从集合里删除
epoll_ctl(epfd,EPOLL_CTL_DEL,rtlEvents[i].data.fd,rtlEvents);
//从用户列表删除
string username;
for (it = userMap.begin(); it != userMap.end(); it++)
{
if (it->second == rtlEvents[i].data.fd)
{
username = it->first;
userMap.erase(it);
break;
}
}
printf("client ip:%s ,port:%u disconnect.\n",connAddr.getIP(),connAddr.getPort());
cout<<"client "<<username<<" exit."<<endl;
}
else
{
//解包
memset(&head,0,sizeof(head));
memcpy(&head,acbuf,HEAD_SIZE);
switch(head.type)
{
case 1:
memset(&login,0,sizeof(login));
memcpy(&login,acbuf + HEAD_SIZE,LOGIN_SIZE);
//通过connfd,区分不同客户端
//如果重复登录,失败,让前一个账号下线 ; 如果登录成功,服务器要发送一个应答包给客户端。
if ( (it = userMap.find(login.name)) != userMap.end())
{
reply = LOGIN_FAIL;
memset(acbuf,0,100);
head.size = 4;
memcpy(acbuf,&head,HEAD_SIZE);
memcpy(acbuf + HEAD_SIZE , &reply , 4);
write(it->second,acbuf,HEAD_SIZE + 4); //登录失败应答包
printf("client %s relogin.\n",login.name);
}
else
{
printf("client %s login.\n",login.name);
}
reply = LOGIN_OK;
memcpy(acbuf + HEAD_SIZE , &reply , 4);
write(rtlEvents[i].data.fd,acbuf,HEAD_SIZE + 4); //登录成功应答包
userMap.insert(pair<string,int>(login.name,rtlEvents[i].data.fd));
break;
case 2:
memset(&chat,0,CHAT_SIZE);
memcpy(&chat,acbuf + HEAD_SIZE,CHAT_SIZE);
if(strcmp(chat.toName,"all") == 0)
{
//群聊
for (it = userMap.begin(); it != userMap.end(); it++)
{
//转发消息
if (it->second != rtlEvents[i].data.fd)
{
write(it->second, acbuf, HEAD_SIZE + CHAT_SIZE);
}
}
}
else
{
//私聊
if ( (it = userMap.find(chat.toName)) != userMap.end()) //找到了
{
//转发消息
write(it->second, acbuf, HEAD_SIZE + CHAT_SIZE);
}
else //用户不存在
{
memset(&chat.msg,0,100);
strcpy(chat.msg,"the acccount is not exist.");
memset(chat.toName,0,10);
memcpy(acbuf + HEAD_SIZE, &chat, CHAT_SIZE);
write(rtlEvents[i].data.fd, acbuf, HEAD_SIZE + CHAT_SIZE);
}
}
break;
}
}
}
}
}
}
return 0;
}
运行结果
至此,我们完成了简单的聊天功能,接下来我们将进一步学习,如何封装socket,并逐步完善功能。
来源:51CTO
作者:SherryX
链接:https://blog.51cto.com/13097817/2066426