C++ socket 网络编程 简单聊天室
操作系统里的进程通讯方式有6种:(有名/匿名)管道、信号、消息队列、信号量、内存(最快)、套接字(最常用),这里我们来介绍用socket来实现进程通讯。
1、简单实现一个单向发送与接收
这是套接字的工作流程
(对于有时间想慢慢看的推荐这篇博客:https://www.cnblogs.com/kefeiGame/p/7246942.html)
(不想自己画一遍,直接用别人的)
我们现在先来实现套接字对同一主机的通讯。(代码注释比较全)
服务器(虚拟机[Ubuntu]):
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h> #define MYPORT 1223///开应一个端口
#define IP "**.**.**.**"///你用的服务器的IPv4地址,这里我用了虚拟机(ubuntu)的地址
#define BACKLOG 10
#define getLen(zero) sizeof(zero) / sizeof(zero[0]) ///得到数组最大大小
using namespace std; int main() {
int sockfd, new_fd;
struct sockaddr_in my_addr;
puts("SERVER:");
if( (sockfd = socket(AF_INET, SOCK_STREAM, )) == - ) {
///socket()函数发生错误则返回-1,否则会返回套接字文件描述符
///对于int socket(int domain, int type, int protocol);中的参数想要详细了解可以看这篇博客:https://blog.csdn.net/liuxingen/article/details/44995467 perror("socket():");///显示错误
return ;
}
my_addr.sin_family = AF_INET;///通讯在IPv4网络通信范围内
my_addr.sin_port = htons(MYPORT);///我的端口
my_addr.sin_addr.s_addr = inet_addr(IP);///用来得到一个32位的IPv4地址,inet_addr将"127.0.0.1"转换成s_addr的无符号整型。
bzero(&(my_addr.sin_zero), getLen(my_addr.sin_zero));///sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。 /**
借用以下代码得到了my_addr.sin_addr.s_addr的类型是无符号整型
unsigned int a;
if(typeid(a) == typeid(my_addr.sin_addr.s_addr)){
puts("Yes");
}
**/ if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -) {///bind()函数将套接字与该IP:端口绑定起来。
perror("bind():");
return ;
}
if(listen(sockfd, BACKLOG) == -) {///启动监听,等待接入请求,BACKLOG是在进入队列中允许的连接数目
perror("listen():");
return ;
} socklen_t sin_size;
struct sockaddr_in their_addr;
if((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -) {
///当你监听到一个来自客户端的connect请求时,要选择是将他放在请求队列里还是允许其连接,我这里写的其实是单进客户的,所以说无等待。
///这个函数还返回了一个新的套接字,用于与该进程通讯。
///还有一点是之前推荐的c++中的socket编程(入门),该博客里写的sin_size类型是int,可是实际上我在linux的C++环境下出现错误,类型要是socklen_t。
perror("accept():");
return ;
}
printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_addr));///inet_ntoa可以将inet_addr函数得到的无符号整型转为字符串IP char str[]; while() {///循环发送 以endS结束与这一进程的通讯,endS也作为客户端停止工作的标志送出
puts("send:");
scanf("%s", str);
if(send(new_fd, str, strlen(str), ) == -) {
///send()函数,new_fd是accept返回的套接字文件描述符,str就你要发送的数据,数据长度,对于最后一位flag
/// flags取值有:
/// 0: 与write()无异(我自己也不知道什么意思,大概就是常规操作,以下提供几种flag值的定义,然后下面是这类宏定义的源码) /// MSG_DONTROUTE 绕过路由表查找。
/// MSG_DONTWAIT 仅本操作非阻塞。
/// MSG_OOB 发送或接收带外数据。
/// MSG_PEEK 窥看外来消息。
/// MSG_WAITALL 等待所有数据。
///
/// 源码里没找到0x00的定义,所以说我将其当作默认参数
/// enum
/// {
/// MSG_OOB = 0x01, /// Process out-of-band data.
/// #define MSG_OOB MSG_OOB
/// MSG_PEEK = 0x02, /// Peek at incoming messages.
/// #define MSG_PEEK MSG_PEEK
/// MSG_DONTROUTE = 0x04, /// Don't use local routing.
/// #define MSG_DONTROUTE MSG_DONTROUTE
/// #ifdef __USE_GNU
/// /// DECnet uses a different name.
/// MSG_TRYHARD = MSG_DONTROUTE,
/// # define MSG_TRYHARD MSG_DONTROUTE
/// #endif
/// MSG_CTRUNC = 0x08, /// Control data lost before delivery.
/// #define MSG_CTRUNC MSG_CTRUNC
/// MSG_PROXY = 0x10, /// Supply or ask second address.
/// #define MSG_PROXY MSG_PROXY
/// MSG_TRUNC = 0x20,
/// #define MSG_TRUNC MSG_TRUNC
/// MSG_DONTWAIT = 0x40, /// Nonblocking IO.
/// #define MSG_DONTWAIT MSG_DONTWAIT
/// MSG_EOR = 0x80, /// End of record.
/// #define MSG_EOR MSG_EOR
/// MSG_WAITALL = 0x100, /// Wait for a full request.
/// #define MSG_WAITALL MSG_WAITALL
/// MSG_FIN = 0x200,
/// #define MSG_FIN MSG_FIN
/// MSG_SYN = 0x400,
/// #define MSG_SYN MSG_SYN
/// MSG_CONFIRM = 0x800, /// Confirm path validity.
/// #define MSG_CONFIRM MSG_CONFIRM
/// MSG_RST = 0x1000,
/// #define MSG_RST MSG_RST
/// MSG_ERRQUEUE = 0x2000, /// Fetch message from error queue.
/// #define MSG_ERRQUEUE MSG_ERRQUEUE
/// MSG_NOSIGNAL = 0x4000, /// Do not generate SIGPIPE.
/// #define MSG_NOSIGNAL MSG_NOSIGNAL
/// MSG_MORE = 0x8000, /// Sender will send more.
/// #define MSG_MORE MSG_MORE
/// MSG_WAITFORONE = 0x10000, /// Wait for at least one packet to return.
/// #define MSG_WAITFORONE MSG_WAITFORONE
/// MSG_BATCH = 0x40000, /// sendmmsg: more messages coming.
/// #define MSG_BATCH MSG_BATCH
/// MSG_ZEROCOPY = 0x4000000, /// Use user data in kernel path.
/// #define MSG_ZEROCOPY MSG_ZEROCOPY
/// MSG_FASTOPEN = 0x20000000, /// Send data in TCP SYN.
/// #define MSG_FASTOPEN MSG_FASTOPEN
///
/// MSG_CMSG_CLOEXEC = 0x40000000 /// Set close_on_exit for file
/// ///descriptor received through
/// ///SCM_RIGHTS.
/// #define MSG_CMSG_CLOEXEC MSG_CMSG_CLOEXEC
/// }; perror("send():");
close(new_fd);///发送失败就关闭该通讯
return ;
}
if(!strcmp("endS", str))
break;
}
close(new_fd);///正常结束要关闭这些已建立的套接字
close(sockfd); return ;
}
linux环境的服务端
客户端(虚拟机[Ubuntu]):(linux环境的客户端)
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h> #define PORT 1223/// 客户机连接远程主机的端口
#define MAXDATASIZE 100 /// 每次可以接收的最大字节
#define IP "**.**.**.**"
#define getLen(zero) sizeof(zero)/sizeof(zero[0])
using namespace std; int main( ) { int sockfd, numbytes;
char buf[MAXDATASIZE];///缓存接收内容
struct sockaddr_in their_addr;///和my_addr用法差不多 puts("USER:");
if((sockfd = socket(AF_INET, SOCK_STREAM, )) == -){
perror("socket():");
return ;
} their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(PORT); their_addr.sin_addr.s_addr = inet_addr(IP);
bzero(&(their_addr.sin_zero),getLen(their_addr.sin_zero));
if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -) {
///在客户端这里我们不需要绑定什么东西,因为我们只要向目的IP:端口发起连接请求 perror("connect():");
return ;
}
while() {///循环接收
if((numbytes=recv(sockfd, buf, MAXDATASIZE, )) == -) {///recv函数,套接字文件描述符,接收到这字符串里,最大长度,flag(之前有解释);
perror("recv():");
return ;
}
buf[numbytes] = '\0';
if(!strcmp(buf, "endS")) {///接收到endS两边一起结束
break;
}
cout<<"Received: "<<buf<<endl;///输出接收的字符
}
close(sockfd);
return ; }
linux环境的客户端
接下来我们把这个客户端移植到windows操作系统下,代码肯定是要有小改动的。但是这个是最后的操作,我们一步步来:
让虚拟机和本机能够ping通(这个我一开始在网络上找 博客,然后都没用,后面把虚拟机的虚拟网络编辑器恢复默认就可以了,所以说建议自己尝试解决);
因为主机和虚拟机可以用IP地址(IPv4)ping通,也就是可以访问该ip,那么我们的服务器就要在那个客户端(主机)可访问的IP上拿一个端口出来用来通讯。
所以说我们服务器的IP地址要选ifconfig指令里看到的虚拟机里的IPv4地址。
接下来开始移植,其实基本思想和代码结构完全没变。
客户端(windows)
#include <iostream>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
///我在codeblocks下不可以运行这些是因为这个libws2_32.a找不到
///解决方法:Settings->compiler->Global compiler settings->(找到)Linker settings(横着排开的目录)->Add->去MinGW/lib找到libws2_32.a就可以了 #define PORT 1223/// 客户机连接远程主机的端口
#define MAXDATASIZE 100 /// 每次可以接收的最大字节
using namespace std; int main( ) {
WORD sockVersion = MAKEWORD(,);
WSADATA wsaData;
if(WSAStartup(sockVersion, &wsaData)!=){
return ;
}
///windows环境下的Winsock是要初始化的;即:固定代码。 int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr; puts("USER:");
if((sockfd = socket(AF_INET, SOCK_STREAM, )) == -){
cout<<WSAGetLastError()<<endl;///这个可以输出WSAError号
perror("socket");
return ;
} their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(PORT); their_addr.sin_addr.s_addr = inet_addr("**.**.**.**");
memset(their_addr.sin_zero, , sizeof(their_addr.sin_zero));
if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -) {
perror("connect");
return ;
}
while() {
if((numbytes=recv(sockfd, buf, MAXDATASIZE, )) == -) {
perror("recv");
return ;
}
buf[numbytes] = '\0';
if(!strcmp(buf, "endS")) {
break;
}
cout<<"Received: "<<buf<<endl;
}
closesocket(sockfd);///函数不同
return ;
}
windows环境的客户端
试着通讯,应该是没问题的!!(至少本地没问题)
不想用windows客户端的人可以用telnet [ip] [port]指令来连接到服务器,前提是服务器使用的ip地址是你能ping通的ip地址
2、实现一个可以发送接收的客户端以及转发消息的服务端
一个聊天室很明显是有多个客户端在一个服务器的协助下进行聊天,就是一个人发一句消息,服务器向所有人发送一遍消息,所有人的客户端接收消息,也就是服务器负责接收转发,客户端也是接收和发送。
接下来我们就要学习一下怎么收发同时进行了,为了实现这一方法,我们可以这样:
我对一个事件(如读或写),不等到发生或者异常就不结束他,服务器读时一直等待客户端的写,反之同理,很显然,这不现实,可能对方并不想回你,但你想发信息给他,但是你此时就做不到。
那么我们就要用I/O多路复用模型了,我先用最简单的select来实现多路复用。(下面简单的从源码解释了poll和select的工作方式)
关于select()函数
这个函数的作用是每次轮询一遍保存好的套接字集合看是否有事件发生(读、写、异常)。
但是因为select每次可以传入的文件描述符集大小只有1024位,所以说这个函数能监听的大小只有1024,至于为什么是1024呢,我们来看看源码对fd_set的定义:
typedef long int __fd_mask;
#define __FD_SETSIZE 1024
#define _NFDBITS (8*(int)sizeof(__fd_mask)) 这个是fd_set内的成员,上面有所需宏定义
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
得到的结论是,成员为:long int fds_bits[ / __NFDBITS],long int字节数为x
则该变量字节数为 x*/x,即不管64位机还是32位机都是1024位。所以说最多保存1024个文件描述符。 poll函数与select函数的不同则是他不是用这样的压位的方式来保存文件描述符,它采用的结构体如下:
struct pollfd{
int fd;///文件描述符
short int events;///轮询时关心的事件种类,种类在下面给源码
short int revents;///实际发生的事件
};
事件的定义
#define POLLIN 0x001 /* 有数据要读 */
#define POLLPRI 0x002 /*有紧急数据要读 */
#define POLLOUT 0x004 /*现在写入不会阻塞 */
# define POLLRDNORM 0x040 /* 可以读正常数据*/
# define POLLRDBAND 0x080 /* 可以读取优先数据 */
# define POLLWRNORM 0x100 /* 现在写入不会阻塞 */
# define POLLWRBAND 0x200 /* 可以写入优先数据 */
/* These are extensions for Linux. */
# define POLLMSG 0x400
# define POLLREMOVE 0x1000
# define POLLRDHUP 0x2000这三个是linux的扩展,有兴趣自己去查,注释是源码里的说明 #define POLLERR 0x008 /*错误条件 */
#define POLLHUP 0x010 /* 挂起 */
#define POLLNVAL 0x020 /* 无效轮询请求 */
看了这些宏定义,接下来就是他的用法:
我关心这个对象的读取状态那么 client.events = POLLIN;
我关系读和写的话client.events = POLLIN | POLLOUT;
判断可读 client.revents & POLLIN 为true就是可读; 之前select用的是一个fd_set 这个poll函数则是传入一个pollfd 指针(即可以是数组)进去:
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
第一个刚刚说明过,第二个是最大文件描述符大小+,第三个是毫秒等待;
这个函数返回:大于零即是发生的事件个数,为零则是超时,-1异常; 这个函数的用法理解了select的用法其实是一样的,当连接的客户端多的时候会产生很大的延迟,因为是每次都轮询的,这个缺点和select一样;
select和poll的分析
select就是将你关系的文件描述符集以及想得到的(读、写、异常)结果集以及等待事件给他,返回给你发生事件的文件描述符的个数,以及(读、写、异常)结果集给你来判断要对该客户端执行何种操作。
直接看服务器代码:(linux环境服务端)
#include <string>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h> #define lisnum 10///最大连接客户端
#define myport 1223///随意的一个(>1024)端口号
#define maxnum 1007///最大字节接收数
#define myip "**.**.**.**"///这个ip可以被ping到就可以用
using namespace std; class keyNode{///这个类存储了连接上的客户端分配的文件描述符以及客户端的昵称
public:
int clientfd;
string name;
keyNode(): clientfd(), name(""){}///构造
void init() {///初始化
this->clientfd=;
this->name="";
}
keyNode& operator = (const keyNode& tmp) {///重载拷贝赋值
if(&tmp != this) {
this->clientfd=tmp.clientfd;
this->name=tmp.name;
}
return *this;
}
}; inline void init(const keyNode client[], const int &sockfd) {///初始化,关闭所有客户端连接
for(int i = ; i < lisnum; ++ i) {
if(client[i].clientfd != ) {
close(client[i].clientfd);
}
}
close(sockfd);
} inline void allSend(const keyNode client[], const char buffer[], const int &maxn, const int &now) {
///将信息送到除了发送者以外的所有客户端上,参数:client[]是自定义类数组,buffer[]是传输字符串,maxn是目前连接数,now是发送者于client[]里的下标
if(buffer[] == '\0')///长度为0不转发
return ;
for(int i = ; i < maxn; ++ i) {
if(i != now)
send(client[i].clientfd, buffer, strlen(buffer), );
}
} int main() {
int sockfd;
struct sockaddr_in my_addr;
if((sockfd = socket(AF_INET, SOCK_STREAM, )) == -) {
perror("socket()");
return ;
}
cout<<"socket ok"<<endl;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(myport);
my_addr.sin_addr.s_addr = inet_addr(myip); memset(my_addr.sin_zero, , sizeof(my_addr.sin_zero));
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -) {
perror("bind()");
return ;
}
puts("bind ok");
if(listen(sockfd, lisnum) == -) {
perror("lisnum()");
return ;
}
cout<<"listen ok"<<endl;
/****************与之前的无异***************************/ fd_set clientfd;///select
clientfd.
int maxsock;///
keyNode IDkey[lisnum];///保存客户端的文件描述符和昵称 int cntfd = ;///记录客户端个数
maxsock = sockfd;//
char buffer[maxnum];
string res;///用来记录一些临时值传输
int len = ;///保存接收字符的长度
while(true) {
FD_ZERO(&clientfd);///将文件描述符集置零 FD_SET(sockfd, &clientfd);///源码中是将0~1023大小的文件描述符直接保存到相应的位里(该位置1) for(int i = ; i < lisnum; ++i) {///将目前有的客户端的文件描述符也加入到集合中
if(IDkey[i].clientfd != ) {
FD_SET(IDkey[i].clientfd, &clientfd);
}
}
if(select(maxsock + , &clientfd, NULL, NULL, NULL) < ) {
///自己没找到select的源码,于是去百度了一波:https://blog.csdn.net/u010601662/article/details/78922557
///第一个参数是所有文件描述符的最大值+1
///第二三四个参数指向文件描述符集(读、写、异常集) ///只有读文件描述集的原因是因为我们的服务器只需要读客户端发的消息,然后转发给其他客户端
///第四个是等待时间,该类有long tv_sec;秒 long tv_usec;毫秒 两个参数
perror("select()");
break;
} for(int i = ; i < cntfd; ++i) {
if(FD_ISSET(IDkey[i].clientfd, &clientfd)) {///检查关心的文件描述符是否有读事件发生
len = recv(IDkey[i].clientfd, buffer, maxnum, );
if(len <= ) {///发生错误 否则正常情况会返回接收到的流的长度
close(IDkey[i].clientfd);
FD_CLR(IDkey[i].clientfd, &clientfd);///将该文件描述符删除 IDkey[i] = IDkey[--cntfd];///最后一位填补上他的空位,保持有用的信息全部连着
IDkey[cntfd].init();
} else {
buffer[len] = '\0';
res = IDkey[i].name + ": " + buffer;///信息发送格式。
allSend(IDkey, res.c_str(), cntfd, i);///全广播
}
}
} if(FD_ISSET(sockfd, &clientfd)) {///检查服务端的文件描述符是否有读事件,有的话表示有新连接请求
struct sockaddr_in client_addr;
socklen_t sizes=;
int sock_client = accept(sockfd, (struct sockaddr*)(&client_addr), &sizes);///接受
if(sock_client < ) {
perror("accept()");
continue;
}
if(cntfd < lisnum) {///只有小于限定大小才让添加
IDkey[cntfd++].clientfd = sock_client; strcpy(buffer, "this is server!\n");
send(sock_client, buffer, strlen(buffer), );///提示信息
///cout<<"new connection client["<<cntfd - 1<<"] "<<inet_ntoa(client_addr.sin_addr)<<":"<<ntohs(client_addr.sin_port)<<endl;
memset(buffer, 0, sizeof(buffer));
len = recv(sock_client, buffer, maxnum, );
if(len < ) {
perror("revc()");
init(IDkey, sockfd);
return ;
}
buffer[len] = '\0';
IDkey[cntfd - ].name = buffer;///我的客户端默认连上第一件事是发送昵称(简陋的实现方法)
strcat(buffer, " join the chatroom\n");
allSend(IDkey, buffer, cntfd, cntfd - );///对客户端提示有人加入了聊天室
maxsock = max(maxsock, sock_client);///更新文件描述符最大值
} else {
cout<<"over the max connections"<<endl;
}
}
}
init(IDkey, sockfd);///全部断开
return ;
}
linux环境的服务端
我们搭建好了一个服务端,也就是说现在就差一堆客户端了!那么我们聊天的时候当然是用中文了,但是linux系统下命令行显示中文的编码会乱码!我也懒得设置什么,于是就想直接写个windows的服务端来用。
windows环境客户端:(自己根据自己的IDE来多开客户端吧!)
#include <thread>
#include <iostream>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib") #define PORT 1223
#define maxnum 100
using namespace std; void recvMsg(const int &sockfd) {///接收信息
char buf[maxnum];
while(true) {
int num = recv(sockfd, buf, maxnum, );
if(num == -) {
perror("recv()");
return ;
}
buf[num]='\0';
cout<<buf<<endl;
}
return ;
}
void init(const int &sockfd) {///发送初始化信息,即昵称
char str[] = "Thanks_up";
if(send(sockfd, str, strlen(str), ) == -) {
perror("send()");
}
return ;
} int main( ) {
WORD sockVersion = MAKEWORD(,);
WSADATA wsaData;
if(WSAStartup(sockVersion, &wsaData)!=){
return ;
} int sockfd, numbytes;
char buf[maxnum];
struct hostent *he; struct sockaddr_in their_addr; if((sockfd = socket(AF_INET, SOCK_STREAM, )) == -){
cout<<WSAGetLastError()<<endl;
perror("socket");
return ;
} their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(PORT);
their_addr.sin_addr.s_addr = inet_addr("**.**.**.**");
memset(their_addr.sin_zero, , sizeof(their_addr.sin_zero)); if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -) {
perror("connect");
return ;
}
char str[];
init(sockfd);
thread taskRecv(recvMsg, sockfd);///将读和写分成两个线程来执行,在分得的时间片内可以看似并行的完成读写任务
taskRecv.detach();///让这个线程不阻塞
while(true) {
scanf("%s", str);
if(send(sockfd, str, strlen(str), ) == -) {
perror("send()");
break;
}
}
closesocket(sockfd);
return ;
}
windows环境的客户端
我测试是没问题的
留下一个linux的客户端:
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define PORT 1223
#define maxnum 100
#define IP "192.168.50.130"
#define getLen(zero) sizeof(zero)/sizeof(zero[0])
using namespace std; void init(const int &sockfd){
if(send(sockfd, "xiejin", , ) == -) {
perror("send()");
close(sockfd);
return ;
}
} void* recvMsg(void* sockid) {
const int sockfd=*((int*)sockid);
char buf[maxnum];
while(true) {
int num = recv(sockfd, buf, maxnum, );
if(num == -) {
perror("recv()");
return ;
}
buf[num]='\0';
cout/**<<"recv: "**/<<buf<<endl;
}
return ;
}
void* sendMsg(void* sockid) {
const int sockfd=*((int*)sockid);
char str[maxnum];
while(true) {
scanf("%s", str);
if(send(sockfd, str, strlen(str), ) == -) {
perror("send()");
close(sockfd);
return ;
}
}
return ;
} int main() { int sockfd, numbytes;
struct sockaddr_in their_addr; puts("USER:");
if((sockfd = socket(AF_INET, SOCK_STREAM, )) == -){
perror("socket():");
return ;
} their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(PORT); their_addr.sin_addr.s_addr = inet_addr(IP);
bzero(&(their_addr.sin_zero),getLen(their_addr.sin_zero));
if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -) {
perror("connect():");
return ;
} init(sockfd); pthread_t thread[];
if(pthread_create(&thread[], NULL, recvMsg, (void *)&(sockfd)) != ) {
perror("pthread_create()");
return ;
}
if(pthread_create(&thread[], NULL, sendMsg, (void *)&(sockfd)) != ) {
perror("pthread_create()");
return ;
}
while(true);
pthread_exit(NULL);
close(sockfd);
return ;
}
linux环境的客户端
既然select的实现方法写完了,那么根据select和poll的相似性我们也可以很轻松的将其更改,想学可以去参考他人的博客,这里就不多做解释了;
3、提升服务器的处理能力
对于一个服务器要是聊天的人一多就会出现严重延迟是绝对不可以的,也就是一个个轮询的方式是费时费力的,那么我们会想办法解决这个问题。
这就涉及到了接下来要讲的epoll。
epoll的底层维护是一颗红黑树,查找和删除修改等等操作都是log级别的,所有很快,具体来说就是一颗红黑树,里面有很多FD,此时来了一个事件,我在树上快速查找有没有与之对应的FD,有就将其添加至list里。然后由下面讲的epoll_wait去等,等待list不为空、收到信号、超时这三种条件后返回一个值。
epoll的操作主要需要这3个接口函数:
int epoll_create(int size);///size是要监听的数目,创建好epoll句柄后,它会占用一个文件描述符,所以说在epoll用完要close(),不然文件描述符可能被耗尽;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/**参数1是上一个函数返回值,2是操作
参数2是操作,操作有三种,EPOLL_CTL_ADD(添加FD),EPOLL_CTL_MOD(修改已添加的FD),EPOLL_CTL_DEL(删除一个FD)
参数3是要对其操作的FD
参数4用来告诉内核需要监听的事件。()
///epoll_event结构体:
struct epoll_event {
unsigned int events;///关注的事件
epoll_data_t data; ///在意这个的用法于是去百度了源码,如果源码没错的话,这个data应该是没被使用过,所以说传入什么参数会原样返回(有误的话请指出,谢谢)
};
typedef union epoll_data {
void *ptr;
int fd;
unsigned int u32;
unsigned long int u64;
}epoll_event_t;
/**
关注的事件:(其他几种有兴趣就去查查)
EPOLLIN = 0x001,///表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLPRI = 0x002,///表示对应的文件描述符有紧急的数据可读
EPOLLOUT = 0x004,///表示对应的文件描述符可以写
EPOLLRDNORM = 0x040,
EPOLLRDBAND = 0x080,
EPOLLWRNORM = 0x100,
EPOLLWRBAND = 0x200,
EPOLLMSG = 0x400,
EPOLLERR = 0x008,///表示对应的文件描述符发生错误
EPOLLHUP = 0x010,///表示对应的文件描述符被挂断
EPOLLRDHUP = 0x2000,
EPOLLEXCLUSIVE = 1u << 28,
EPOLLWAKEUP = 1u << 29,
EPOLLONESHOT = 1u << 30,///只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
EPOLLET = 1u << 31///将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
**/
struct epoll_event详细
**/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);///返回产生事件的个数,且maxevents不能大于epoll_create()的size,timeout是ms为单位,为0立即返回,-1将不确定或永久阻塞。一般返回0表示超时
这个是一个实例,将上面的select改为了epoll的做法,但是因为嫌麻烦就没写昵称的接收,客户端可以继续用上面select的客户端。
值得注意的是linux2.6才有开始有epoll的方法。(有一些可能知道ET以及LT的小伙伴可能会发现我在发消息前没有将events修改为EPOLLOUT之类的,但是我默认的情况是缓冲区不会满,也就是不需要通知,只管发送,然后客户端一直接收)
#include <set>
#include <vector>
#include <string>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define LISNUM 10
#define MYPORT 1223
#define MAXLEN 1007
#define MYIP "**.**.**.**"
using namespace std; set<int> socketset;///维护现有的socketfd,用于全发送
vector<int> deletefd;///存储异常的文件描述符 int socketBind( );///socket()、bind() void doEpoll(int &sockfd); void handleEvents(int &epollfd, struct epoll_event *events, int &num, int &sockfd, char *buffer); void handleAccept(int &epollfd, int &sockfd); void handleRecv(int &epollfd, int &sockfd, char *buffer); void allSend(char buffer[], int &nowfd, int &epollfd); void handleSend(int &epollfd, int &sockfd, char *buffer); void addEvent(int &epollfd, int &sockfd, int state); void deleteEvent(int &epollfd, int sockfd, int state); void modifyEvent(int &epollfd, int &sockfd, int state); int main( ) {
int sockfd = socketBind( );
if(listen(sockfd, LISNUM) == -) {
perror("listen()");
return ;
}
cout<<"listen ok"<<endl;
doEpoll(sockfd);
return ;
} int socketBind( ){///socket()、bind()
int sockfd;
struct sockaddr_in my_addr;
if((sockfd = socket(AF_INET, SOCK_STREAM, )) == -){
perror("socket()");
exit();
}
cout<<"socket ok"<<endl;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = inet_addr(MYIP); memset(my_addr.sin_zero, , sizeof(my_addr.sin_zero)); if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -) {
perror("bind()");
exit();
}
puts("bind ok");
return sockfd;
} void doEpoll(int &sockfd) {
int epollfd = epoll_create(LISNUM);///创建好一个epoll后会产生一个fd值
struct epoll_event events[LISNUM]; int ret;
char buffer[MAXLEN]={};
addEvent(epollfd, sockfd, EPOLLIN);///对sockfd这个连接,我们关心的是是否有客户端要连接他,所以说要将读事件设为关心 while(true) {///持续执行
ret = epoll_wait(epollfd, events, LISNUM, -);
handleEvents(epollfd, events, ret, sockfd, buffer);///对得到的事件进行处理
}
close(epollfd);
} void handleEvents(int &epollfd, struct epoll_event *events, int &num, int &sockfd, char *buffer){
int listenfd;
for(int i = ; i < num; ++i) {
listenfd = events[i].data.fd;
if((listenfd == sockfd)&&(events[i].events & EPOLLIN)) {
handleAccept(epollfd, sockfd);///处理客户端连接请求
} else if(events[i].events & EPOLLIN) {
handleRecv(epollfd, listenfd, buffer);///处理客户端发送的信息 }
}
} void handleAccept(int &epollfd, int &sockfd) {
int clientfd;
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = ;
if((clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &clientaddrlen)) == -) {
perror("accept()");
} else {
socketset.insert(clientfd);
addEvent(epollfd, clientfd, EPOLLIN);///处理连接,我们关心这个连接的读事件
}
} void handleRecv(int &epollfd, int &sockfd, char *buffer) {
int len = recv(sockfd, buffer, MAXLEN, );
if(len <= ) {
perror("recv()");
socketset.erase(sockfd);
deleteEvent(epollfd, sockfd, EPOLLIN);
} else {
cout<<buffer<<endl;
allSend(buffer, sockfd, epollfd);///成功接收到一个字符串就转发给全部客户端
}
} void allSend(char buffer[], int &nowfd, int &epollfd) {
///modifyEvent(epollfd, nowfd, EPOLLOUT);
if(buffer[] == '\0')
return ;
for(auto it = socketset.begin(); it != socketset.end() ; ++ it) {
if(*it != nowfd){
cout<<"__"<<buffer<<"________"<<endl;
if(send(*it, buffer, strlen(buffer), ) == -) {
perror("send()");
deletefd.push_back(*it);///直接erase会导致迭代器失效 deleteEvent(epollfd, *it, EPOLLIN);
}
}
}
for(size_t i = ; i < deletefd.size(); ++i) { ///单独删除
socketset.erase(deletefd[i]);
}
deletefd.clear();
///modifyEvent(epollfd, nowfd, EPOLLIN);
memset(buffer, , MAXLEN);
} void addEvent(int &epollfd, int &sockfd, int state) {
struct epoll_event ev;
ev.events=state;
ev.data.fd = sockfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
} void deleteEvent(int &epollfd, int sockfd, int state) {
struct epoll_event ev;
close(sockfd);
ev.events=state;
ev.data.fd = sockfd;
epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, &ev);
} void modifyEvent(int &epollfd, int &sockfd, int state) {
struct epoll_event ev;
ev.events=state;
ev.data.fd = sockfd;
epoll_ctl(epollfd, EPOLL_CTL_MOD, sockfd, &ev);
}
linux环境的服务端
4、继续提升处理能力
我们知道了epoll是很优秀的I/O多路复用的方法了,但是其实还是有问题的,最大的问题就是无并发。(并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行)
为什么无并发是最大的问题呢!我们可以想象,默认情况下,其实大部分函数都是阻塞的,即:一个函数没执行完毕那么程序就不能继续运行下去。
那么为了解决这个问题我们就会想用让他们并发,比如我的代码里的allSend其实可以让他一遍自个儿传去的,还有接收也一样,以及accept。那么我们就可以接收到一条消息就新建一个线程用来allSend给其他客户端,其他操作同理。(一般我们一个线程里只处理一个socket,因为每一个socket都是阻塞的)(这里谈到的方法就是Reactor模式,只不过我只不过是泛泛之谈,具体可以看这里:https://www.cnblogs.com/doit8791/p/7461479.html)
怎么做确实极大的提高了效率,因为程序不再是单线程一直被某些操作所阻塞的状态了。但是如果连接数高的情况下呢??很明显,我们要一直开线程和关线程,虽然说线程的创建以及销毁开销远远小于进程的创建销毁的开销,但是数量一大也会需要大量的系统资源,系统可能吃不消。那么我们就要限制创建线程的数量,此时我们就要引入一个线程池的概念。
线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。(百度百科)
PS:我的线程池是在实验楼学的,那个是收费课程,想着直接发出来不太好,就只这样说一下思路。
C++ socket 网络编程 简单聊天室的更多相关文章
- Socket网络编程--简单Web服务器(6)
本来是想实现ssl连接的,但是弄了好久都不成功,就索性不做了,等以后有能力再做了.所以这一小节就是本次的最后一节了.就简单的说几个注意点. 1.加个配置文件 使用单例模式,使用一个类,该类保存一些信息 ...
- Java网络编程案例---聊天室
网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来. java.net包中JavaSE的API包含有类和接口,它们提供低层次的通信细节.你可以直接使用这些类和接口,来专注于解决 ...
- Java网络编程--简单聊天程序
背景 毕业设计前的练手,学校小比赛中的一个题目. 开发环境 Java(eclipse)+Mysql 简介 使用Java+Mysql开发以个简单的聊天工具,在本次项目中实现了: 1. 用户登录(客户端至 ...
- Socket网络编程--简单Web服务器(1)
这一次的Socket系列准备讲Web服务器.就是编写一个简单的Web服务器,具体怎么做呢?我也不是很清楚流程,所以我找来了一个开源的小的Web服务器--tinyhttpd.这个服务器才500多行的代码 ...
- Socket网络编程--简单Web服务器(2)
上一小节通过阅读开源的Web服务器--tinyhttpd.大概知道了一次交互的请求信息和应答信息的具体过程.接下来我就自己简单的实现一个Web服务器. 下面这个程序只是实现一个简单的框架出来.这次先实 ...
- Socket网络编程--简单Web服务器(3)
上一小节已经实现了浏览器发送请求,然后服务器给出应答信息,然后浏览器显示出服务器发送过来的网页.一切看起来都是那么的美好.这一小节就准备实现可以根据地址栏url的不同来返回指定的网页.目前还不考虑带参 ...
- Socket网络编程--简单Web服务器(4)
上一小节已经实现了对图片的传输,接下来就是判断文件是否为js,css,png等格式.我们增加一个函数用于判断格式 int WebServer::get_filetype(char *type,char ...
- Socket网络编程--简单Web服务器(5)
这一小节我们将实现服务器对get和post的请求进行对cgi程序的调用.对于web服务器以前的章节已经实现了对get和post请求的调用接口,接下来给出对应接口的实现. int WebServer:: ...
- Android笔记——Socket通信实现简单聊天室
两部分,客户端和服务端 ---------------------------------------------------------------- 客户端 1.为防止ANR异常,互联网连接可用 ...
随机推荐
- linux内核分析--操作系统是如何工作的?
一个简单的时间片轮转多道程序 操作系统的"两把剑":中断上下文(保存现场和恢复现场)和进程上下文的切换 源代码的分析 *使用的源代码为视频中所使用的精简内核的源代码 首先分析myp ...
- 《Linux内核分析》课程第四周学习总结
姓名:何伟钦 学号:20135223 ( *原创作品转载请注明出处*) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/U ...
- 每日scrum(6)
今天是小组正式冲刺的第六天,软件的各种结尾工作,还有一些模块就已经全部实现了: 遇到的问题主要是对于自己能力的担忧,以前总是想,如果自己努力,就会怎样成功,其实并不是那样,小小的距离就是很远的能力差距 ...
- spring注入 属性注入 构造器注入 set方法注入
spring注入 属性注入 构造器注入 set方法注入(外部bean注入)
- 设置close
- java的类属性默认有this 但容易与参数重名 所以需要显性的加上this 以分区别
- 实现AJAX的基本步骤
实现AJAX的基本步骤 要完整实现一个AJAX异步调用和局部刷新,通常需要以下几个步骤: (1)创建XMLHttpRequest对象,也就是创建一个异步调用对象. (2)创建一个新的HTTP请求,并指 ...
- java 前台使用枚举方法(一)
枚举值封装: http://blog.csdn.net/hanjun0612/article/details/72845960 一 基本类型 这里接着说前台调用枚举值. 首先,controller层 ...
- MySQL relay_log_purge=0 时的风险
转自: http://xiezhenye.com/2015/12/mysql-relay_log_purge0-%E6%97%B6%E7%9A%84%E9%A3%8E%E9%99%A9.html 有时 ...
- 【题解】 bzoj1864: [Zjoi2006]三色二叉树 (动态规划)
bzoj1864,懒得复制,戳我戳我 Solution: 其实想出来了\(dp\)方程推出来了最大值,一直没想到推最小值 \(dp[i][1/0]\)表示\(i\)号节点的子树中的绿色染色最大值,\( ...