重点:socket共用方法中错误码的定义以及错误码的解析
底层辅助代码
//serhelp.h #ifndef _vxser #define _vxser #ifdef __cplusplus extern "C" { #endif /** * sersocket_init - socket初始化 * @listenfd:文件描述符 * 成功返回0,失败返回错误码 * */ int sersocket_init(int *listenfd); /** * listen_socket - 绑定端口号,监听套接字 * @listenfd:文件描述符 * @port:绑定的端口号 * 成功返回0,失败返回错误码 * */ int listen_socket(int listenfd, int port); /** * run_server - 运行服务器 * @listenfd:文件描述符 * */ void run_server(int listenfd); #ifdef __cplusplus } #endif #endif
//sockhelp.c //socket发送接收底层辅助方法 /*底层辅助方法不打印错误信息,由上层调用通过errno打印信息,并且不做参数验证,有调用函数验证*/ #include "sockhelp.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <sys/select.h> #include <fcntl.h> /** * readn - 读取指定大小的字节 * @fd:文件描述符 * @buf:接收字节缓冲区 * @count:指定的字节数 * 成功返回指定字节数,失败返回-1,对方连接已经关闭,返回已经读取字节数<count * */ int readn(int fd, void *buf, int count) { //定义剩余字节数 int lread = count; //定义每次读取的字节数 int nread = 0; //定义辅助指针变量 char *pbuf = (char *) buf; //如果剩余字节数大于0,循环读取 while (lread > 0) { nread = read(fd, pbuf, lread); if (nread == -1) { //read()是可中断睡眠函数,需要屏蔽信号 if (errno == EINTR) continue; //read()出错,直接退出 return -1; } else if (nread == 0) { //对方关联连接 return count - lread; } //重置剩余字节数 lread -= nread; //辅助指针变量后移 pbuf += nread; } return count; } /** * writen - 写入指定大小的字节 * @fd:文件描述符 * @buf:发送字节缓冲区 * @count:指定的字节数 * 成功返回指定字节数,失败返回-1 * */ int writen(int fd, void *buf, int count) { //剩余字节数 int lwrite = count; //每次发送字节数 int nwrite = 0; //定义辅助指针变量 char *pbuf = (char *) buf; while (lwrite > 0) { nwrite = write(fd, pbuf, lwrite); if (nwrite == -1) { //注意:由于有TCP/IP发送缓存区,所以即使对方关闭连接,发送也不一定会失败 //所以需要捕捉SIGPIPE信号 return -1; } lwrite -= nwrite; pbuf += nwrite; } return count; } /** * read_timeout - 读超时检测函数,不含读操作 * @fd:文件描述符 * @wait_seconds:等待超时秒数,如果为0表示不检测超时 * 成功返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT * */ int read_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { //定义文件描述符集合 fd_set readfds; //清空文件描述符 FD_ZERO(&readfds); //将当前文件描述符添加集合中 FD_SET(fd, &readfds); //定义时间变量 struct timeval timeout; timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, &readfds, NULL, NULL, &timeout); } while (ret == -1 && errno == EINTR); //ret==-1时,返回的ret正好就是-1 if (ret == 0) { errno = ETIMEDOUT; ret = -1; } else if (ret == 1) { ret = 0; } } return ret; } /** * write_timeout - 写超时检测函数,不含写操作 * @fd:文件描述符 * @wait_seconds:等待超时秒数,如果为0表示不检测超时 * 成功返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT * */ int write_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { //定义文件描述符集合 fd_set writefds; //清空集合 FD_ZERO(&writefds); //添加文件描述符 FD_SET(fd, &writefds); //定义时间变量 struct timeval timeout; timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, NULL, &writefds, NULL, &timeout); } while (ret == -1 && errno == EINTR); if (ret == 0) { errno = ETIMEDOUT; ret = -1; } else if (ret == 1) { ret = 0; } } return ret; } /** * accept_timeout - 带超时accept (方法中已执行accept) * @fd:文件描述符 * @addr:地址结构体指针 * @wait_seconds:等待超时秒数,如果为0表示不检测超时 * 成功返回已连接的套接字,失败返回-1,超时返回-1并且errno = ETIMEDOUT * */ int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { /* * 说明:accept和connect都会阻塞进程,accept的本质是从listen的队列中读一个连接,是一个读事件 * 三次握手机制是由TCP/IP协议实现的,并不是由connect函数实现的,connect函数只是发起一个连接, * connect并非读写事件,所以只能设置connect非阻塞,而用select监测写事件(读事件必须由对方先发送报文,时间太长了) * 所以accept可以由select管理 * 强调:服务端套接字是被动套接字,实际上只有读事件 * */ fd_set readfds; FD_ZERO(&readfds); FD_SET(fd, &readfds); struct timeval timeout; timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, &readfds, NULL, NULL, &timeout); } while (ret == -1 && errno == EINTR); if (ret == -1) { return -1; } else if (ret == 0) { ret = -1; errno = ETIMEDOUT; return ret; } //成功无需处理,直接往下执行 } //一旦检测出select有事件发生,表示有三次握手成功的客户端连接到来了 //此时调用accept不会被阻塞 if (addr != NULL) { socklen_t len = sizeof(struct sockaddr_in); ret = accept(fd, (struct sockaddr *) addr, &len); } else { ret = accept(fd, NULL, NULL); } return ret; } /** * activate_nonblock - 设置套接字非阻塞 * @fd:文件描述符 * 成功返回0,失败返回-1 * */ int activate_nonblock(int fd) { int ret = 0; int flags = fcntl(fd, F_GETFL); if (flags == -1) return -1; flags = flags | O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) return -1; return ret; } /** * deactivate_nonblock - 设置套接字阻塞 * @fd:文件描述符 * 成功返回0,失败返回-1 * */ int deactivate_nonblock(int fd) { int ret = 0; int flags = fcntl(fd, F_GETFL); if (flags == -1) return -1; flags = flags & (~O_NONBLOCK); ret = fcntl(fd, F_SETFL, flags); if (ret == -1) return -1; return ret; } /** * connect_timeout - 带超时的connect(方法中已执行connect) * @fd:文件描述符 * @addr:地址结构体指针 * @wait_seconds:等待超时秒数,如果为0表示不检测超时 * 成功返回0.失败返回-1,超时返回-1并且errno = ETIMEDOUT * */ int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret = 0; //connect()函数是连接服务器,本来connect会阻塞,但是设置未阻塞之后, //客户端仍然会三次握手机制,如果三次握手失败,那么客户端一定无法向文件描述符中写入数据 //如果连接成功,那么客户端就可以向文件描述符写入数据了, //所以交给select监管的文件描述符如果可以写,说明连接成功,不可以写说明连接失败 //设置当前文件描述符未阻塞--设置非阻塞之后, //connect在网络中非常耗时,所以需要设置成非阻塞,如果有读事件,说明可能连接成功 //这样有利于做超时限制 if (wait_seconds > 0) { if (activate_nonblock(fd) == -1) return -1; } ret = connect(fd, (struct sockaddr *) addr, sizeof(struct sockaddr)); if (ret == -1 && errno == EINPROGRESS) { fd_set writefds; FD_ZERO(&writefds); FD_SET(fd, &writefds); struct timeval timeout; timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, NULL, &writefds, NULL, &timeout); } while (ret == -1 && errno == EINTR); //ret==-1 不需要处理,正好给ret赋值 //select()报错,但是此时不能退出当前connect_timeout()函数 //因为还需要取消文件描述符的非阻塞 if (ret == 0) { errno = ETIMEDOUT; ret = -1; } else if (ret == 1) { //ret返回为1(表示套接字可写),可能有两种情况,一种是连接建立成功,一种是套接字产生错误, //此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 int err = 0; socklen_t len = sizeof(err); ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); if (ret == 0 && err != 0) { errno = err; ret = -1; } //说明套接字没有发生错误,成功 } } if (wait_seconds > 0) { if (deactivate_nonblock(fd) == -1) return -1; } return ret; }
socket共用代码
//commsocket.h #include "sockhelp.h" #ifndef _vx2016 #define _vx2016 /* * 思考:select超时应该用在客户端,客户对时间有要求, * 客户端不一定支持select,并且客户端IO也不多,所以管理IO使用多进程 * 服务器不需要使用select超时,但是需要select管理客户端连接和监听套接字 * */ //定义错误码 #define OK 0 #define Sck_BaseErr 3000 #define Sck_MacErr (Sck_BaseErr+1) #define Sck_TimeoutErr (Sck_BaseErr+2) #define Sck_ParamErr (Sck_BaseErr+3) #define Sck_PipeClosed (Sck_BaseErr+4) #define MAXBUFSIZE 1020 //留出4个字节存放包体大小 //定义粘包结构 typedef struct _packet { int len; //报文长度 char buf[MAXBUFSIZE]; //包体 } Packet; //定义socket结构 typedef struct _mysock { int fd; } Mysock; #ifdef __cplusplus extern "C" { #endif /** * strsockerr - 错误码转成字符串 * @err:错误码 * 返回错误信息 * */ char * strsockerr(int err); /** * socket_send - 报文发送 * @fd:文件描述符 * @buf:写入缓冲区 * @buflen:写入数据长度 * 成功返回0,失败返回-1 * */ int socket_send(int fd, void *buf, int buflen); /** * socket_recv - 报文接收 * @fd:文件描述符 * @buf:接收缓冲区 * @buflen:接收数据长度 * 成功返回0,失败返回-1 * */ int socket_recv(int fd, void *buf, int *buflen); /** * InstallSignal - 安装信号 * @signarr:信号数组 * @len:信号数组的长度 * 成功返回0,失败返回错误码 * */ int Install_Signal(int *signarr, int len,void (*handler)(int)); #ifdef __cplusplus extern "C" } #endif #endif
//commsocket.c -- socket上层方法实现 #include "commsocket.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> /** * strsockerr - 错误码转成字符串 * @err:错误码 * 返回错误信息 * */ char * strsockerr(int err) { switch (err) { case OK: return "success!"; case Sck_BaseErr: return "方法内部错误!"; case Sck_MacErr: return "malloc内存错误!"; case Sck_TimeoutErr: return "select 超时错误!"; case Sck_ParamErr: return "方法参数列表错误!"; case Sck_PipeClosed: return "对等方已经关闭连接!"; default: return "未识别错误码!"; } } /** * socket_send - 报文发送 * @fd:文件描述符 * @buf:写入缓冲区 * @buflen:写入数据长度 * 成功返回0,失败返回错误码 * */ int socket_send(int fd, void *buf, int buflen) { int ret = 0; Packet pack; memset(&pack, 0, sizeof(pack)); //本地字节序转化成网络字节序 pack.len = htonl(buflen); strncpy(pack.buf, buf, MAXBUFSIZE); ret = writen(fd, &pack, buflen + 4); if (ret == -1) { ret=Sck_BaseErr; perror("writen() err"); return ret; } else if (ret == buflen) { ret = 0; } return ret; } /** * socket_recv - 报文接收 * @fd:文件描述符 * @buf:接收缓冲区 * @buflen:接收数据长度 * 成功返回0,失败返回错误码 * */ int socket_recv(int fd, void *buf, int *buflen) { int ret = 0; //定义包体长度 int len = *buflen; int hostlen = 0; Packet pack; memset(&pack, 0, sizeof(pack)); //获取报文字节数 ret = readn(fd, &pack.len, 4); if (ret == -1) { perror("readn() err"); return -1; } else if (ret < 4) { printf("peer is closed !\n"); return -1; } //网络字节转化成本地字节序 hostlen = ntohl(pack.len); if (len < hostlen) { printf("socket_recv() 接收缓冲区太小!\n"); return -1; } ret = readn(fd, pack.buf, hostlen); if (ret == -1) { perror("readn() err"); return -1; } else if (ret < hostlen) { printf("peer is closed !\n"); return -1; } *buflen = hostlen; strncpy(buf, pack.buf, hostlen); return ret; } /** * InstallSignal - 安装信号 * @signarr:信号数组 * @len:信号数组的长度 * 成功返回0,失败返回错误码 * */ int Install_Signal(int *signarr, int len,void (*handler)(int)) { int ret = 0; if (signarr == NULL) { ret = -1; printf("Install_Signal() param not correct !\n"); return ret; } int i = 0; for (i = 0; i < len; i++) { //安装信号 if (signal(signarr[i], handler) == SIG_ERR) { ret = -1; printf("signal() failed !\n"); break; } } return ret; }