之前使用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,并逐步完善功能。