关于socket:c语言实现一个简单的web服务器借助http协议

这个程序是看到的一本书上socket编程章节的课后题,题目内容很多,具体可见链接:https://www.bookstack.cn/read...实现一个简略的Web服务器myhttpd。服务器程序启动时要读取配置文件/etc/myhttpd.conf,其中须要指定服务器监听的端口号和服务目录,我设置的是如下:Port=8000Directory=/var/www在Directory即服务器的/var/www放入你想要加载到client(此处是浏览器)的文件,如图片或者是html文件。能够看到这是我的ecs外面放的要加载的文件。 多过程传输的server的代码如下所示,留神是在linux环境下编程和执行! #include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#include <sys/wait.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <fcntl.h>#define RECV_PORT 8000#define QLEN 10#define MAX_SIZE 1024char browser_com_buf[256]; int mylisten (void){ int listenfd; int err; socklen_t addrlen; struct sockaddr_in server_addr; char *address = "101.132.101.121"; listenfd = socket ( AF_INET, SOCK_STREAM, 0 ); /*creat a new socket*/ if ( listenfd < 0 ) { printf ( "listen error\n" ); return -1; } memset ( &server_addr, 0, sizeof ( server_addr ) ); server_addr.sin_family = AF_INET; /*IPV4*/ server_addr.sin_port = htons ( RECV_PORT ); server_addr.sin_addr.s_addr = htonl ( INADDR_ANY ); addrlen = sizeof ( server_addr ); err = bind ( listenfd, ( struct sockaddr * )&server_addr, addrlen ); if ( err < 0 ) { printf ( "bind error\n" ); return -2; } err = listen ( listenfd, QLEN ); if ( err < 0 ) { printf ( "listen error" ); return -3; } return listenfd;}int myaccept ( int listenfd ){ int clientfd; int err; socklen_t addrlen; struct sockaddr_in client_addr; addrlen = sizeof ( client_addr ); clientfd = accept ( listenfd, ( struct sockaddr * )&client_addr, &addrlen ); if ( clientfd < 0 ) { printf ( "accept error\n" ); return -1; } return clientfd;}void browser_com_analysis ( char *com, char *buf ){ int i = 0; unsigned int flag = 0; char *locate; locate = strchr ( com, '/' ); //顺次检索com字符串中的每一个字符,直到遇见字符 /,返回第一次呈现字符/的地位 if ( locate == NULL ) { printf ( "not find\n" ); exit ( 1 ); } else { //把命令逐字符存到buf中 while ( *locate != ' ' ) { buf[i++] = *locate; locate++; } buf[i] = '\0'; } //printf ( "%s\n", buf );}//判断文件的权限和状态,是纯txt/html还是图片,还是CGI程序(可执行文件)int state_estimat ( char *buf ){ int len; unsigned int flag = 0; int i = 0, j = 0; char buf1[256]; char *image_style = ".jpg"; char file_name[256] = { "/var/www" }; struct stat statbuf; len = strlen ( buf ); memset ( buf1, '\0', sizeof ( buf1 ) ); while ( i < len ) { //将文件格式后缀的内容存到buf[1],如test.jpg将.jpg存到buf1中 if ( buf[i] == '.' ) { flag = 1; } if ( flag == 1 ) { if ( buf[i] == ' ' || buf[i] == '\n' ) { break; } else { buf1[j++] = buf[i]; } } i++; } //printf ( "%s%d\n", buf1, len ); if ( len == 0 ) { printf ( "http have not send comand\n" ); exit ( 1 ); } //GET 之后只有一个命令/,所以len == 1,在本例中/实际上是/var/www //Web服务器应该把该目录下的索引页(默认是index.html)发给浏览器. //也就是把/var/www/index.html发给浏览器 else if ( len == 1 ) { return 1; } else if ( len > 1 ) { //将参数 src 字符串复制到参数 dest 所指的字符串尾部;dest 最初的完结字符 NULL 会被笼罩掉. //并在连贯后的字符串的尾部再减少一个 NULL。 strcat ( file_name, buf ); stat ( file_name, &statbuf ); //stat()用来将参数file_name 所指的文件状态, 复制到参数statbuf所指的构造中 if ( statbuf.st_mode & S_IXOTH ) //其余用户具备可执行权限 { //如果具备可执行权限,则执行该文件,将后果发送到浏览器上 return 4; } else { //不具备可执行权限则判断图片还是文件 if ( strcmp ( buf1, image_style ) == 0 ) { return 2; //浏览器申请的是图片 } return 3; //浏览器申请的是文件 } }}int find_file ( char *buf, int clientfd ){ int state; char my_file_buf[65536 * 2]; int n; char fault[] = { "HTTP/1.1 404 Not Found\nContent-Type: text/html\n<html><body>request file not found</body></html>\n\n" }; char head[] = { "HTTP/1.1 200 OK\nContent-Type: text/html\n\n" }; char head2[] = { "HTTP/1.1 200 OK\nContent-Type: image/jpg\n\n" }; char head3[] = { "HTTP/1.1 200 OK\n\n" }; char file_name[] = { "/var/www" }; memset ( &my_file_buf, '\0', MAX_SIZE ); state = state_estimat ( buf ); //Web服务器应该把该目录下的索引页(默认是index.html) if ( state == 1 ) //server send file to browser { int filefd; filefd = open ( "/var/www/index.html", O_RDWR );//返回0示意胜利 //参数 pathname 指向欲关上的文件门路/var/www/index.html. if ( filefd < 0 ) { printf ( "find file failed\n" ); write ( clientfd, fault, strlen ( fault ) ); close ( clientfd ); } else { n = read ( filefd, my_file_buf, MAX_SIZE );//胜利返回读到的字节数 //read()会把参数fd 所指的文件传送count个字节到buf 指针所指的内存中 if ( n < 0 ) { printf ( "read the root file failed\n" ); exit ( 1 ); } strcat ( head, my_file_buf );//将从文件获取到的内容放到head头内容 的上面 printf ( "%s", head ); write ( clientfd, head, strlen ( head ) );//胜利返回理论写入的字节数 //把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内 close ( clientfd ); } } else if ( state == 2 ) { printf ( "picture\n" ); int picture_fd; int n; write ( clientfd, head2, strlen ( head2 ) ); memset ( &my_file_buf, '\0', sizeof ( my_file_buf ) ); strcat ( file_name, buf ); picture_fd = open ( file_name, O_RDONLY ); //只读关上图片文件 if ( picture_fd < 0 ) { printf ( "open picture failed\n" ); exit ( 1 ); } n = read ( picture_fd, my_file_buf, sizeof ( my_file_buf ) ); //将fd所指的文件传送count个字节到my_file_buf,此处文件是图片,所以传的是二进制码 if ( n < 0 ) { printf ( "read picture data failed\n" ); exit ( 1 ); } write ( clientfd, my_file_buf, sizeof ( my_file_buf ) ); close ( clientfd ); } else if ( state == 3 ) { //3是指一般不可执行文件 printf ( "file\n" ); int file_fd; int n; write ( clientfd, head, strlen ( head ) );//head是“text/html” memset ( &my_file_buf, '\0', sizeof ( my_file_buf ) ); strcat ( file_name, buf );//buf是指浏览器申请的文件名放在file_name /var/www之后 file_fd = open ( file_name, O_RDONLY ); if ( file_fd < 0 ) { printf ( "open unCGI-file failed\n" ); exit ( 1 ); } n = read ( file_fd, my_file_buf, sizeof ( my_file_buf ) ); if ( n < 0 ) { printf ( "read unCGI-file failed \n" ); exit ( 1 ); } write ( clientfd, my_file_buf, sizeof ( my_file_buf ) ); } else if ( state == 4 ) { //是可执行文件 printf ( "executable file\n" ); pid_t pid; pid = fork (); if ( pid < 0 ) { printf ( "creat child failed\n" ); exit ( 1 ); } else if ( pid > 0 ) //parent { int stateval; waitpid ( pid, &stateval, 0 ); //waitpid()会临时进行目前过程的执行, wait有信号来到或子过程完结而后彻底清除该子过程 //如果在调用wait()时子过程曾经完结, 则wait()会立刻返回子过程完结状态值. //子过程的完结状态值会由参数status返回 //wait期待第一个终止的子过程,而waitpid能够通过pid参数指定期待哪一个子过程 close ( clientfd ); } else //child用来执行该可执行文件并返回后果到浏览器输入 { int err; char *argv[1];//char* argv[1]指针数组,只有一个指针元素 strcat ( file_name, buf ); argv[1] = file_name; dup2 ( STDOUT_FILENO, clientfd ); //用dup2重定向子过程的规范输入到客户端socket err = execv ( "/bin/bash", argv );//执行CGI 如/bin/bash test.sh //其规范输入会向dup2指定的那样定向到浏览器 //不带字母p(示意path)的exec函数第一个参数必须是程序的相对路径或绝对路径 if ( err < 0 ) { printf ( "executable file failed\n" ); exit ( 1 ); } close ( clientfd ); } }}int main ( int argc, char *argv[] ){ int listenfd, clientfd; int filefd, n; char recvbuf[MAX_SIZE]; char data_buf[MAX_SIZE]; listenfd = mylisten(); if ( listenfd < 0 ) { exit ( 0 ); } printf ( "listening and listen fd is %d......\n", listenfd ); while ( 1 ) { clientfd = myaccept ( listenfd ); printf ( "accept success......\n" ); if ( clientfd < 0 ) { exit ( 1 ); } memset ( &recvbuf, '\0', MAX_SIZE ); memset ( &data_buf, '\0', MAX_SIZE ); n = read ( clientfd, recvbuf, MAX_SIZE ); printf ( "read OK\n" ); if ( n < 0 ) { printf ( "read error\n" ); exit ( 2 ); } filefd = open ( "/tmp/read_html.txt", O_RDWR | O_CREAT | O_TRUNC ); //open o new file write ( filefd, recvbuf, n ); //save the http information to filefd browser_com_analysis ( recvbuf, data_buf );//将收到的命令解析出/+前面有用的保留在data_buf find_file ( data_buf, clientfd );//对浏览器的申请内容进行判断,是一般html还是picture或是CGI } close ( clientfd ); return 0;}让咱们来看一下成果:1、首先在linux中运行该程序,此处我应用的vscode近程连贯linux,能够间接运行server,如果你是在centos默认的界面或者vim编辑,能够应用gcc http_sever.c -o http_sever生成可执行文件 和 ./http_sever来执行该文件,能够看到服务器开始listen监听是否有连贯申请: ...

April 9, 2022 · 6 min · jiezi

关于socket:windows本地SOCKET通信

业务场景是主控机A和数据卡B通过UDP通信。因为软件测试,迫于环境须要本地过程间通信。间接将原先跑在两台设施的软件跑在同一台机器,置单方IP都为127.0.0.1。原先主控机和数据卡的接管端口都为6002,现同一台设施运行,两个过程端口不能够雷同。将主控机的接管端口改为6003,即可失常通信。代码如下 UDP初始化库/*-----------------------------------------------------------**** 文件名: UDP_Driver.c**** 形容: 定义了通过UDP进行通信所需的所有函数和变量**** 定义的函数:** UDP_Init()** UDP_Send()** UDP_Recv()**** 设计注记:********-----------------------------------------------------------*//*UDP运行环境*/#ifdef _WIN32#define UDP_UNDER_VXWORKS 0#else#define UDP_UNDER_VXWORKS 2 /*Windows C++运行环境下定义为0, vxWorks5.x环境下定义为1, vxWorks6.x环境下定义为2*/#endif#if UDP_UNDER_VXWORKS > 0//#include <sysIO.h>#include <netinet/in.h>#include <sockLib.h>#include <string.h>#include <stdio.h>#include <fcntl.h>#include "errnoLib.h"#include "UDP_Driver.h"#if UDP_UNDER_VXWORKS == 2#ifndef SOCKADDR //typedef struct sockaddr SOCKADD#endif#endiftypedef unsigned int SOCKET;#else#include <WinSock2.h>#include <stdio.h>#pragma comment(lib, "WS2_32.lib")#include "UDP_Driver.h"typedef char* caddr_t;static WSADATA wsaData;#endiftypedef struct sockaddr_in SockAddr_TYPE;typedef struct{ int (*Send)(void * const Handler, void * const Payload, int const LenBytes); int (*Recv)(void * const Handler, void * const Buffer, int const MaxBytes); SOCKET Recv_Socket;/*接管和发送的socket*/ SockAddr_TYPE Self_Recv_Address;/*配置接管IP与端口号,用于创立Recv_Socket*/ SockAddr_TYPE Peer_Send_Address;/*配置源端IP与端口号,用于断定接收数据起源*/ SockAddr_TYPE Peer_Recv_Address;/*配置目标IP与端口号,用于指定发送地址*/} UDP_INNER_TYPE;/*.BH-------------------------------------------------------------**** 函数名: UDP_Send**** 形容: UDP发送函数**** 输出参数:** Handler : void * const, UDP句柄** Payload : void * const, 发送数据指针** LenBytes: int const, 发送数据长度**** 输入参数:** 返回值: int, > 0, 发送胜利(返回发送数据长度, 单位:BYTE)** -1, 句柄有效** -2, Payload为空或LenBytes小于1** -3, Socket故障**** 设计注记:****.EH-------------------------------------------------------------*/int UDP_Send(void * const Handler, void * const Payload, int const LenBytes){ UDP_INNER_TYPE * pUDP = (UDP_INNER_TYPE*)Handler; int ret_val = 0; if (pUDP == NULL) ret_val = -1; else if ((Payload == NULL) || (LenBytes < 1)) ret_val = -2; else { ret_val = sendto(pUDP->Recv_Socket/*pUDP->Send_Socket*/, (caddr_t)Payload, LenBytes, 0, (SOCKADDR*)&pUDP->Peer_Recv_Address, sizeof(SockAddr_TYPE));#if UDP_UNDER_VXWORKS > 0 if (ret_val == ERROR)#else if (ret_val == SOCKET_ERROR)#endif { //printf(" UDP_Send Error %s\n", strerror(errno)); //printf("UDP_Send errno = %d\n", WSAGetLastError()); ret_val = -3; } } return ret_val;}/* END of UDP_Send *//*.BH-------------------------------------------------------------**** 函数名: UDP_Recv**** 形容: UDP接管函数**** 输出参数:** Handler : void * const, UDP句柄** Buffer : void * const, 寄存接收数据的缓存指针** MaxBytes: int const, Buffer的最大容量**** 输入参数:** 返回值: int, >= 1, 理论获取到的数据长度(单位:BYTE)** 0, 无数据可接管** -1, 句柄有效** -2, Buffer为空或MaxBytes长度有余** -3, Socket故障**** 设计注记:****.EH-------------------------------------------------------------*/int UDP_Recv(void * const Handler, void * const Buffer, int const MaxBytes){ UDP_INNER_TYPE * pUDP = (UDP_INNER_TYPE*)Handler; SockAddr_TYPE Source_Address; int Source_AddrSize = sizeof(SockAddr_TYPE); int ret_val = 0; if (pUDP == NULL) ret_val = -1; else if ((Buffer == NULL) || (MaxBytes < 1)) ret_val = -2; else { while ((ret_val = recvfrom(pUDP->Recv_Socket, (char*)Buffer, MaxBytes, 0, (SOCKADDR*)&Source_Address, &Source_AddrSize)) > 0) { if ((0xC00A0050 == pUDP->Peer_Send_Address.sin_addr.s_addr) /*&& (Source_Address.sin_port == pUDP->Peer_Send_Address.sin_port)*/) { // just for test()MMP Send Data, data monitor in wireshark, but IDMP have no data recived! } /*对端往用XXXX发数据,本端就往对端的这个端口XXXX发数据(默认认为对端收发接口统一)*/ if ((Source_Address.sin_addr.s_addr == pUDP->Peer_Send_Address.sin_addr.s_addr) /*&& (Source_Address.sin_port == pUDP->Peer_Send_Address.sin_port)*/) { //printf("[UDP_Recv] recv port %d, %d\n", htons(Source_Address.sin_port), htons(pUDP->Self_Recv_Address.sin_port)); pUDP->Peer_Recv_Address.sin_port = Source_Address.sin_port; break; } }#if 0 ret_val = recvfrom(pUDP->Recv_Socket, (char*)Buffer, MaxBytes, 0, (SOCKADDR*)&Source_Address, &Source_AddrSize); if(ret_val > 0){ if ((Source_Address.sin_addr.s_addr == pUDP->Peer_Send_Address.sin_addr.s_addr) /*&& (Source_Address.sin_port == pUDP->Peer_Send_Address.sin_port)*/) { pUDP->Peer_Recv_Address.sin_port = Source_Address.sin_port; } }#endif if (ret_val < 0){ ret_val = -3; } } return ret_val;}/* END of UDP_Recv *//*.BH-------------------------------------------------------------** ** 函数名: UDP_Init**** 形容: UDP初始化接口函数**** 输出参数:** Handler : UDP_STRUCT_TYPE * const, 需初始化的UDP句柄** Peer_IP : unsigned int const, 对方设施IP地址** Peer_Send_Port : unsigned short const, 对方设施发送UDP端口号** Peer_Recv_Port : unsigned short const, 对方设施接管UDP端口号** Self_IP : unsigned int const, 本方设施IP地址(单网卡环境可置零,多网卡环境必须指定通信网卡地址)** Self_Send_Port : unsigned short const, 本方设施发送UDP端口号** Self_Recv_Port : unsigned short const, 本方设施接管UDP端口号** Recv_Cache : int const, 接管缓存大小(零碎默认为8192, 大数据量时可减少)** Recv_Block_Mode: int const, 接管模式: 0-阻塞, 1-非阻塞**** 输入参数:** 返回值: int, >= 0, 初始化胜利** -1, 初始化失败(句柄有效)**** 设计注记:****.EH-------------------------------------------------------------*/int UDP_Init(UDP_STRUCT_TYPE * const Handler, unsigned int const Peer_IP, unsigned short const Peer_Send_Port, unsigned short const Peer_Recv_Port, unsigned int const Self_IP, unsigned short const Self_Send_Port, unsigned short const Self_Recv_Port, int const Recv_Cache, int const Recv_Block_Mode){ UDP_INNER_TYPE *pUDP = (UDP_INNER_TYPE*)Handler; unsigned int Recv_Mode; int Ret;#if UDP_UNDER_VXWORKS == 0 static int First_Init = 1; if (First_Init) WSAStartup(MAKEWORD(2, 2), &wsaData); First_Init = 0;#endif if (pUDP == NULL) return -1; /*send buffer默认100000*/ // if(0 == getsockopt(pUDP->Send_Socket, SOL_SOCKET, SO_SNDBUF, buffer, &Send_Cache)) // printf("udp send buff size %d\n", *(int*)buffer); pUDP->Self_Recv_Address.sin_family = AF_INET; pUDP->Self_Recv_Address.sin_addr.s_addr = htonl(Self_IP); pUDP->Self_Recv_Address.sin_port = htons(Self_Recv_Port); pUDP->Recv_Socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); Ret = bind(pUDP->Recv_Socket, (SOCKADDR*)&(pUDP->Self_Recv_Address), sizeof(SockAddr_TYPE)); if (Ret < 0) { printf("Bind Failed, Ret-%d, sock-%d\n", Ret, pUDP->Recv_Socket); return -2; } if (Recv_Cache > 8192){ setsockopt(pUDP->Recv_Socket, SOL_SOCKET, SO_RCVBUF, (char*)&Recv_Cache, sizeof(int)); setsockopt(pUDP->Recv_Socket, SOL_SOCKET, SO_SNDBUF, (char*)&Recv_Cache, sizeof(int)); } Recv_Mode = (Recv_Block_Mode > 0);#if UDP_UNDER_VXWORKS > 0 ioctl(pUDP->Recv_Socket, FIONBIO, &Recv_Mode);#else ioctlsocket(pUDP->Recv_Socket, FIONBIO, (u_long*)&Recv_Mode);#endif pUDP->Peer_Send_Address.sin_family = AF_INET; pUDP->Peer_Send_Address.sin_addr.s_addr = htonl(Peer_IP); pUDP->Peer_Send_Address.sin_port = htons(Peer_Send_Port); pUDP->Peer_Recv_Address.sin_family = AF_INET; pUDP->Peer_Recv_Address.sin_addr.s_addr = htonl(Peer_IP); pUDP->Peer_Recv_Address.sin_port = htons(Peer_Recv_Port); pUDP->Send = UDP_Send; pUDP->Recv = UDP_Recv; return 0;}/* END of UDP_Init */#undef SockAddr_TYPE#if UDP_UNDER_VXWORKS > 0 #undef SOCKADDR #undef SOCKET#else #undef caddr_t#endif#undef UDP_UNDER_VXWORKS主控机初始化(A):#define DLR_CARD_IP 0x7F000001/*0xAC100A0F*/#define DLR_RECV_IDMP_PORT 6002#define IDMP_RECV_DLR_PORT 6003/*6002*//*BASEIO_UDP0_CHANNEL_DLR:服务器端(DLR)IP地址:172.16.10.15*/Ret = UDP_Init(&UDP_Handler[0], DLR_CARD_IP, DLR_RECV_IDMP_PORT, DLR_RECV_IDMP_PORT, IDMP_SIM_IP1, 0, IDMP_RECV_DLR_PORT, 64 * 1024, UDP_RECV_NONBLK);if (Ret != 0) { IOM_Report_Fault(Chn++, CDMP_FAIL); USR_LOG(LOGER_IOSS, LOG4C_PRIORITY_ERROR, "%s, %d UDP_Init Error %d\n", "DLR", 0, Ret);}else { IOM_Report_Fault(Chn++, CDMP_PASS);}数据卡端初始化(B):#define UdpLocalIP 0x7F000001#define DLR_RECV_IDMP_PORT 6002#define IDMP_RECV_DLR_PORT 6003/*6002*/Ret = UDP_Init(&UDP_Handler[BASEIO_UDP0_CHANNEL_DLR], UdpLocalIP/*0xAC100A10*/, IDMP_RECV_DLR_PORT, IDMP_RECV_DLR_PORT, UdpLocalIP, DLR_RECV_IDMP_PORT, DLR_RECV_IDMP_PORT, 64 * 1024, UDP_RECV_NONBLK);if (Ret != 0) { //IOM_Report_Fault(Chn++, CDMP_FAIL); USR_LOG(LOGER_IOSS, LOG4C_PRIORITY_ERROR, "MCU UDP_Init Error %d\n", Ret);}这样,一个点对点的本地过程间UDP通信模型就建设实现了。不论是A发B还是B发A,都能够失常通信。如果谋求效率能够将SOCKET绑定为AF_UNIX,通过文件进行过程间数据收发。 ...

January 6, 2022 · 4 min · jiezi

关于socket:详解什么是-socket套接字插座

 欢送大家搜寻“小猴子的技术笔记”关注我的公众号,有问题能够及时和我交换。 你晓得插座吗?你晓得网络编程中的插座吗?兴许你会有点蛊惑,什么是插座!然而我如果说出“套接字”、“socket”这样的关键字你就会豁然开朗。 所谓的“插座”叫做套接字又叫做socket,用来示意一个端点,能够与网络中其余的socket进行连贯,而后进行数据的传输。 咱们都晓得在网络上中能够通过IP地址确定惟一的一台主机,而后主机和主机之间进行通信。然而精确来说:网络通讯中的单方并不是主机,而是主机中的过程。这就须要确定主机中那个过程进行的网络通讯,因而还须要一个端口号来确定主机中的惟一过程。 如果从操作系统的角度来说:套接字是应用规范的Unix文件描述符来与其余计算机进行通信的一种形式。 IP+PORT的组合就形成了网络中惟一标识符“套接字”。端口号的范畴是0-65535,然而低于256的端口号进行了保留,作为零碎的规范的应用程序端口。比方HTTP的默认的80,POP3的110,Telent的23,FTP的21等。 套接字容许两个过程进行通信,这两个过程可能运行在同一个机器上,也可能运行在不同机器上。 套接字次要有两类:流式套接字和数据报套接字。 流式套接字提供了面向连贯、牢靠的数据传输服务,能够十分精确的实现依照程序接收数据。如果你通过流式套接字发送"h","e","l","l","o"五个字符,它达到另一端的程序也将会是"h","e","l","l","o"。起因就在于流式套接字应用的是TCP进行数据传输,可能保证数据的安全性。流式套接字是最常应用的,一些家喻户晓的协定应用的都是它,如HTTP,TCP,SMTP,POP3等。 数据报套接字:提供无连贯的服务,你不须要像流式套接字那样建设一个连贯,而只须要将地址信息一起打包而后收回去。该服务应用的是UDP进行传输,提早小,效率高然而不能保障数据传输的准确性。 如果咱们创立一个ServerSocket须要经验以下几个步骤:bind-->listen-->accpet-->recv-->write-->close。这也是底层Linux/或者Unix对一个端口监听经验的步骤。 bind:绑定一个地址和端口,确认端点的信息。 ServerSocket serverSocket = new ServerSocket(8888); 这里兴许你看到了并没有指明IP,那么“ServerSocket”会主动获取本机的ip地址作为填充,源码如下: public ServerSocket(int port) throws IOException { this(port, 50, null);}public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException { setImpl(); if (port < 0 || port > 0xFFFF) throw new IllegalArgumentException("Port value out of range: " + port); if (backlog < 1) backlog = 50; try { bind(new InetSocketAddress(bindAddr, port), backlog); } catch(SecurityException e) { close(); throw e; } catch(IOException e) { close(); throw e; }}public InetSocketAddress(InetAddress addr, int port) { holder = new InetSocketAddressHolder( null, addr == null ? InetAddress.anyLocalAddress() : addr, checkPort(port));} 会对“InetAddress”进行判断,如果为空了就获取本地的地址。 ...

February 19, 2021 · 1 min · jiezi

关于socket:网络与socket

https://blog.csdn.net/zhihuiy...这篇文章写的也不错,socket={ip,port}临时不了解过程为什么要用ip+协定+port作为惟一标识 目录 网络IP地址端口socket总结1 网络1.1 网络定义网络就是一种辅助单方或者多方可能连贯在一起的工具如果没有网络可想单机的世界是如许的孤独1.2 应用网络的目标就是为了联通多方而后进行通信用的,即把数据从一方传递给另外一方如果没有网络,编写的程序都是单机的,即不能和其余电脑上的程序进行通信为了让在不同的电脑上运行的软件,之间可能相互传递数据,就须要借助网络的性能1.3 总结应用网络可能把多方链接在一起,而后能够进行数据传递所谓的网络编程就是,让在不同的电脑上的软件可能进行数据传递,即过程之间的通信2 IP地址2.1 ip地址的作用ip地址:用来在网络中标记一台电脑,比方192.168.1.1;在本地局域网上是惟一的。 2.2 ip地址的分类每一个IP地址包含两局部:网络地址和主机地址 2.2.1 A类IP地址一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”地址范畴1.0.0.1-126.255.255.254二进制示意为: 00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110 可用的A类网络有126个,每个网络能包容1677214个主机 2.2.2 B类IP地址一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”地址范畴128.1.0.1-191.255.255.254二进制示意为: 10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110 可用的B类网络有16384个,每个网络能包容65534主机 2.2.3 C类IP地址一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”范畴192.0.1.1-223.255.255.254二进制示意为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110 C类网络可达2097152个,每个网络能包容254个主机 2.2.4 D类地址用于多点播送D类IP地址第一个字节以“1110”开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点播送(Multicast)中多点播送地址用来一次寻址一组计算机 s 地址范畴 224.0.0.1-239.255.255.254 2.2.5 E类IP地址以“1111”开始,为未来应用保留E类地址保留,仅作试验和开发用 2.2.6 公有ip在这么多网络IP中,国内规定有一部分IP地址是用于咱们的局域网应用,也就是属于私网IP,不在公网中应用的,它们的范畴是: 10.0.0.0~10.255.255.255172.16.0.0~172.31.255.255192.168.0.0~192.168.255.255 留神: IP地址127.0.0.1~127.255.255.255用于回路测试,如:127.0.0.1能够代表本机IP地址,用 http://127.0.0.1 就能够测试本机中配置的Web服务器。3 端口3.1 什么是端口端口就好一个房子的门,是出入这间房子的必经之路。如果一个程序须要收发网络数据,那么就须要有这样的端口在linux零碎中,端口能够有65536(2的16次方)个之多!对端口进行对立编号,这就是所说的端口号3.2 端口号端口是通过端口号(一个整数)来标记的,范畴是从0到65535留神: 端口数不一样的*nix零碎不一样,还能够手动批改3.3 端口的调配端口号不是随便应用的,而是依照肯定的规定进行调配。端口的分类规范有好几种,笔者在这里不做具体解说,只介绍一下出名端口和动静端口 3.3.1 出名端口(Well Known Ports)出名端口是 家喻户晓的端口号,范畴从0到1023 ...

February 6, 2021 · 1 min · jiezi

关于socket:linux-socket编程不限于linux一切皆socket

“所有皆Socket!” 话虽些许夸大,然而事实也是,当初的网络编程简直都是用的socket。 ——有感于理论编程和开源我的项目钻研。 咱们深谙信息交换的价值,那网络中过程之间如何通信,如咱们每天关上浏览器浏览网页时,浏览器的过程怎么与web服务器通信的?当你用QQ聊天时,QQ过程怎么与服务器或你好友所在的QQ过程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的根本函数,这些都是本文想介绍的。本文的次要内容如下: 1、网络中过程之间如何通信?2、Socket是什么?3、socket的基本操作  3.1、socket()函数 3.2、bind()函数 3.3、listen()、connect()函数 3.4、accept()函数 3.5、read()、write()函数等 3.6、close()函数 4、socket中TCP的三次握手建设连贯详解5、socket中TCP的四次握手开释连贯详解6、一个例子(实际一下)7、留下一个问题,欢送大家回帖答复!!!如果感觉文章对你有帮忙,无妨给我点个关注知乎:秃顶之路  b站:linux亦有归途   每天都会更新咱们的公开课录播以及编程干货和大厂面经或者间接点击链接https://ke.qq.com/course/4177...来课堂上跟咱们讲师面对面交换须要大厂面经跟学习纲要的小伙伴能够加群973961276获取 1、网络中过程之间如何通信?本地的过程间通信(IPC)有很多种形式,但能够总结为上面4类: 消息传递(管道、FIFO、音讯队列)同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)共享内存(匿名的和具名的)近程过程调用(Solaris门和Sun RPC) 但这些都不是本文的主题!咱们要探讨的是网络中过程之间如何通信?首要解决的问题是如何惟一标识一个过程,否则通信无从谈起!在本地能够通过过程PID来惟一标识一个过程,然而在网络中这是行不通的。其实TCP/IP协定族曾经帮咱们解决了这个问题,网络层的“ip地址”能够惟一标识网络中的主机,而传输层的“协定+端口”能够惟一标识主机中的应用程序(过程)。这样利用三元组(ip地址,协定,端口)就能够标识网络的过程了,网络中的过程通信就能够利用这个标记与其它过程进行交互。 应用TCP/IP协定的应用程序通常采纳利用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(曾经被淘汰),来实现网络过程之间的通信。就目前而言,简直所有的应用程序都是采纳socket,而当初又是网络时代,网络中过程通信是无处不在,这就是我为什么说“所有皆socket”。 2、什么是Socket?下面咱们曾经晓得网络中的过程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux根本哲学之一就是“所有皆文件”,都能够用“关上open –> 读写write/read –> 敞开close”模式来操作。我的了解就是Socket就是该模式的一个实现,socket即是一种非凡的文件,一些socket函数就是对其进行的操作(读/写IO、关上、敞开),这些函数咱们在前面进行介绍。 socket一词的起源在组网畛域的首次应用是在1970年2月12日公布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。依据美国计算机历史博物馆的记录,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口形成一个连贯的一端,而一个连贯可齐全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大概12年。” 3、socket的基本操作既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。上面以TCP为例,介绍几个根本的socket接口函数。 3.1、socket()函数 int socket(int domain, int type, int protocol); socket函数对应于一般文件的关上操作。一般文件的关上操作返回一个文件形容字,而socket()用于创立一个socket描述符(socket descriptor),它惟一标识一个socket。这个socket形容字跟文件形容字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。 正如能够给fopen的传入不同参数值,以关上不同的文件。创立socket的时候,也能够指定不同的参数创立不同的socket描述符,socket函数的三个参数别离为: domain:即协定域,又称为协定族(family)。罕用的协定族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协定族决定了socket的地址类型,在通信中必须采纳对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。type:指定socket类型。罕用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。protocol:故名思意,就是指定协定。罕用的协定有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们别离对应TCP传输协定、UDP传输协定、STCP传输协定、TIPC传输协定(这个协定我将会独自开篇探讨!)。 留神:并不是下面的type和protocol能够随便组合的,如SOCK_STREAM不能够跟IPPROTO_UDP组合。当protocol为0时,会主动抉择type类型对应的默认协定。 当咱们调用socket创立一个socket时,返回的socket形容字它存在于协定族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时零碎会主动随机调配一个端口。 3.2、bind()函数 正如下面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 函数的三个参数别离为: sockfd:即socket形容字,它是通过socket()函数创立了,惟一标识一个socket。bind()函数就是将给这个形容字绑定一个名字。addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协定地址。这个地址构造依据地址创立socket时的地址协定族的不同而不同,如ipv4对应的是:struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ }; ipv6对应的是: struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ }; Unix域对应的是: #define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ }; ...

October 29, 2020 · 2 min · jiezi

那些年我们忽略的socket参数

调试过网络程序的人大多使用过tcpdump,那你知道tcpdump是如何工作的吗? tcpdump这类工具也被称为Sniffer,它可以在不影响应用程序正常报文的情况下,将流经网卡的报文复制一份给Sniffer,然后经过加工过滤,最后呈现给用户。 本文不分析tcpdump的具体实现,而只是借tcpdump来揭示一些网络编程中一个大多数人都容易忽略的一个主题:Socket参数对用户接收报文的影响... 相信所有接触过Socket编程的人都应该认识下面这个API #include <sys/socket.h>sockfd = socket(int socket_family, int socket_type, int protocol);没错,它基本是socket编程的第一步,创建一个套接字。他有三个参数,不过又有多少人真的去了解这些参数的意义呢? 对于TCP或者UDP应用的开发者来说,他们可以很容易地从互联网上找(抄)到这样的例子: /* 创建TCP socket*/sockfd = socket(AF_INET, SOCK_STREAM, 0);/* 创建UDP socket*/sockfd = socket(AF_INET, SOCK_DGRAM, 0)为什么第一个参数要使用AF_INET,为什么第二个参数要使用SOCK_STREAM或者SOCK_DGRAM,为什么第三个参数要填0 ? socket_family第一个参数表示创建的socket所属的地址簇或者协议簇,取值以AF或者PF开头定义在(include\linux\socket.h),实际使用中并没有区别(有两个不同的名字只是因为是历史上的设计原因)。最常用的取值有AF_INET,AF_PACKET,AF_UNIX等。AF_UNIX用于主机内部进程间通信,本文暂且不谈。AF_INET与AF_PACKET的区别在于使用前者只能看到IP层以上的东西,而后者可以看到链路层的信息。 什么意思呢? 为了说明这个问题,我们需要知道网络报文的分类。如下图所示:Ethernet II帧是应用最为广泛的帧类型(当然也有像PPP这样的其他链路帧类型)。Ethernet II帧内部,又可大致分为IP报文和其他报文。我们熟悉的TCP或者UDP报文都属于IP报文。 AF_INET是与IP报文对应的,而AF_PACKET则是与Ethernet II报文对应的。AF_INET创建的套接字称为inet socket,而AF_PACKET创建的套接字称为packet socket socket_type & protocol第一个参数family会影响第二个参数socket_type和第三个参数protocol取值范围 第二个参数socket_type表示套接字类型。它的取值不多,常见的就以下三种 enum sock_type { SOCK_STREAM = 1, /* stream (connection) socket */ SOCK_DGRAM = 2, /* datagram (conn.less) socket */ SOCK_RAW = 3, /* raw socket */};第三个参数protocol表示套接字上报文的协议。 ...

August 17, 2019 · 2 min · jiezi

多进程可以监听同一端口吗

还是先来看下man文档中是怎么说的: SO_REUSEPORT (since Linux 3.9) Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address. This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket. To prevent port hijacking, all of the pro‐ cesses binding to the same address must have the same effec‐ tive UID. This option can be employed with both TCP and UDP sockets. For TCP sockets, this option allows accept(2) load distribu‐ tion in a multi-threaded server to be improved by using a dis‐ tinct listener socket for each thread. This provides improved load distribution as compared to traditional techniques such using a single accept(2)ing thread that distributes connec‐ tions, or having multiple threads that compete to accept(2) from the same socket. For UDP sockets, the use of this option can provide better distribution of incoming datagrams to multiple processes (or threads) as compared to the traditional technique of having multiple processes compete to receive datagrams on the same socket.从文档中可以看到,该参数允许多个socket绑定到同一本地地址,即使socket是处于listen状态的。 ...

August 8, 2019 · 3 min · jiezi

socket的SOREUSEADDR参数全面分析

在具体分析之前,我们先看下socket(7)的man文档对这个参数是怎么介绍的: SO_REUSEADDR Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a spe‐ cific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.从这段文档中我们可以知道三个事: ...

August 7, 2019 · 5 min · jiezi

tcp-使用socket模拟分析tcp协议

写两段简单的python代码,然后来抓包分析tcp协议 服务端IP:172.16.196.145客户端IP:172.16.196.142 TCP三次握手、四次挥手server端代码import sockets = socket.socket()s.bind(('172.16.196.145',60000))s.listen(5)while 1: conn, addr = s.accept() date = conn.recv(1024) if date == 'get': conn.send('200 ok') conn.close() print 'Connected by', addr, 'now closed'client端代码import sockets = socket.socket()s.connect(('172.16.196.145',60000))s.send('get')print s.recv(1024)s.close()tcpdump抓包1 12:43:46.905723 IP 172.16.196.142.41334 > 172.16.196.145.60000: Flags [S], seq 3255498564, win 14600, options [mss 1460,sackOK,TS val 1412272238 ecr 0,nop,wscale 7], length 02 12:43:46.905751 IP 172.16.196.145.60000 > 172.16.196.142.41334: Flags [S.], seq 4195434198, ack 3255498565, win 14480, options [mss 1460,sackOK,TS val 1425611003 ecr 1412272238,nop,wscale 7], length 03 12:43:46.905987 IP 172.16.196.142.41334 > 172.16.196.145.60000: Flags [.], ack 4195434199, win 115, options [nop,nop,TS val 1412272238 ecr 1425611003], length 04 12:43:46.906031 IP 172.16.196.142.41334 > 172.16.196.145.60000: Flags [P.], seq 3255498565:3255498568, ack 4195434199, win 115, options [nop,nop,TS val 1412272238 ecr 1425611003], length 35 12:43:46.906041 IP 172.16.196.145.60000 > 172.16.196.142.41334: Flags [.], ack 3255498568, win 114, options [nop,nop,TS val 1425611003 ecr 1412272238], length 06 12:43:46.906265 IP 172.16.196.145.60000 > 172.16.196.142.41334: Flags [P.], seq 4195434199:4195434205, ack 3255498568, win 114, options [nop,nop,TS val 1425611003 ecr 1412272238], length 67 12:43:46.906305 IP 172.16.196.145.60000 > 172.16.196.142.41334: Flags [F.], seq 4195434205, ack 3255498568, win 114, options [nop,nop,TS val 1425611003 ecr 1412272238], length 08 12:43:46.906406 IP 172.16.196.142.41334 > 172.16.196.145.60000: Flags [.], ack 4195434205, win 115, options [nop,nop,TS val 1412272239 ecr 1425611003], length 09 12:43:46.906487 IP 172.16.196.142.41334 > 172.16.196.145.60000: Flags [F.], seq 3255498568, ack 4195434206, win 115, options [nop,nop,TS val 1412272239 ecr 1425611003], length 010 12:43:46.906500 IP 172.16.196.145.60000 > 172.16.196.142.41334: Flags [.], ack 3255498569, win 114, options [nop,nop,TS val 1425611004 ecr 1412272239], length 0逐行分析第一行: 客户端172.16.196.142,端口41336向服务端172.16.196.145端口60000发起SYN主动请求,seq:3255498564第二行: 服务端172.16.196.145.60000给客户端172.16.196.142.41336确认ACK ack为3255498564+1=3255498565,并同时也发起SYN同步第三行: 客户端回复服务端的SYN确认,三次握手建立连接第四行: 客户端调用s.send('get'),发送数据,因此为P标记(PST),seq:3255498565:3255498568第五行: 服务端回复客户端ACK确认标记,ack:3255498568第六行: 服务端调用conn.send('200 ok')给客户端回复数据,为P标记(PST),seq:4195434199:4195434205第七行: 服务端发送回复数据后,关闭连接,发起FIN主动关闭,seq:4195434205第八行: 客户端回复服务端数据(第六行)的确认ACK,ack:4195434205第九行: 客户端发送FIN关闭连接,seq:3255498568 ack:4195434206第十行: 服务端发送客户端FIN的确认,由此,整个连接关闭 ack:3255498569 ...

July 3, 2019 · 4 min · jiezi

Go-socket实现多语言间通信

前言socket提供了在传输层进行网络编程的方法,它比基于http协议的接口传输更高效,RPC(Remote Procedure Call)是远程过程调用,常用于分布式系统等,而rpc很多是基于socket实现的。不了解socket、http等协议请阅读 https://blog.csdn.net/guyan03...。 Socket 都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。它不局限于某一语言,这里我们主要讲Go、php语言基于socket实现通讯。 序列化采用json,数据格式比较简单,支持多种语言。 Go socket 采用c/s架构 客户端:net.Dial() Write() Read() Close() 服务器:net.Listen() Accept() Read() Write() Close() 源代码地址:https://github.com/guyan0319/...测试1、下载源代码至GOPATH目录golangSocketPhp 2、运行服务端,在example目录下server.go go run server.php输出: Waiting for clients 3、新窗口下运行客户端,在example目录下client.go go run client.go输出: receive data string[6]:golang golang这个是从服务端返回的数据。 4、运行php语言客户端,在php目录下的socket_client.php php -f socket_client.php或浏览器访问 http://localhost/xxx/socket_c... 配置自己的网址 输出结果: client write successserver return message is:php 小结:选json序列化,主要考虑它实现起来简单,很多语言支持。缺点是序列化效率低,序列化后数据相对比较大(这里跟protobuf对比)。 links目录

June 27, 2019 · 1 min · jiezi

socket和accept返回的套接字fd有什么区别

记录unix网络编程的复习之路简单回顾下socket连接过程socket() --得到fd! 功能:指定了协议族(IPv4、IPv6或unix)和套接口类型(字节流、数据报或原始套接口)。但并没有指定本地协议地址或远程协议地址。 定义:int socket(int family, int type, int protocol); 返回:出错:-1 成功:套接口描述字 (socket file descriptor)(套接字)sockfdbind() --我在哪个端口? 功能:给套接口分配一个本地协议地址。 定义:int bind(int sockfd, const struct sockaddr *my_addr, int addrlen);connect() --Hello! 功能:建立与TCP服务器的连接 定义:int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);listen() --有人给我打电话吗? 功能:将未连接主动套接口的转换为被动套接口,指示内核接受对该套接口的连接请求。 定义:int listen(int sockfd, int backlog); 参数: - sockfd调用socket函数返回的文件描述符(套接字). - 未完成连接队列和已完成连接队列的上限. - 未完成连接队列 : 服务端还未完成三次握手全部过程的一个队列. - 已完成连接队列 : 服务端已经完成三次握手全部过程的一个队列, 等待accept函数从这个队列中返回下一个(返回其实是取出, 该套接字不在已完成队列中了)套接字.accept() --"Thank you for calling port 3490." ...

June 13, 2019 · 1 min · jiezi

为什么kill进程后socket一直处于FINWAIT1状态

本文介绍一个因为conntrack内核参数设置和iptables规则设置的原因导致TCP连接不能正常关闭(socket一直处于FIN_WAIT_1状态)的案例,并介绍conntrack相关代码在conntrack表项超时后对新报文的处理逻辑。 案例现象问题的现象: ECS上有一个进程,建立了到另一个服务器的socket连接。 kill掉进程,发现tcpdump抓不到FIN包发出,导致服务器端的连接没有正常关闭。为什么有这种现象呢? 梳理正常情况下kill进程后,用户态调用close()系统调用来发起TCP FIN给对端,所以这肯定是个异常现象。关键的信息是: 用户态kill进程。ECS网卡层面没有抓到FIN包。从这个现象描述中可以推断问题出在位于用户空间和网卡驱动中间的内核态中。但是是系统调用问题,还是FIN已经构造后出的问题,还不确定。这时候比较简单有效的判断的方法是看socket的状态。socket处于TIME_WAIT_1状态,这个信息很有用,可以判断系统调用是正常的,因为按照TCP状态机,FIN发出来后socket会进入TIME_WAIT_1状态,在收到对端ACK后进入TIME_WAIT_2状态。关于socket的另一个信息是:这个socket长时间处于TIME_WAIT_1状态,这也反向证明了在网卡上没有抓到FIN包的陈述是合理。FIN包没出虚机网卡,对端收不到FIN,所以自然没有机会回ACK。 真凶问题梳理到了这里,基本上可以进一步聚焦了,在没有大bug的情况下,需要重点看下iptables(netfilter), tc等机制对报文的影响。果然在ECS中有许多iptables规则。利用iptables -nvL可以打出每条rule匹配到的计数,或者利用写log的办法,示例如下: # 记录下new state的报文的日志iptables -A INPUT -p tcp -m state --state NEW -j LOG --log-prefix "[iptables] INPUT NEW: "在这个案例中,通过计数和近一步的log,发现了是OUTPUT chain的最后一跳DROP规则被匹配上了,如下: # iptables -A OUTPUT -m state --state INVALID -j DROP问题的真凶在此时被找到了:iptables规则丢弃了kill进程后发出的FIN包,导致对端收不到,连接无法正常关闭。 到了这里,离最终的root cause还有两个疑问: 问题是否在全局必现?触发的条件是什么?为什么FIN包被认为是INVALID状态?何时触发先来看第一个问题:问题是否在全局必现?触发的条件是什么? 对于ECS上与服务器建立TCP连接的进程,问题实际上不是每次必现的。建议用netcat来做测试,验证下是否是全局影响。通过测试,有如下发现: 利用netcat做类似的操作,也能复现同样的问题,说明这个确实是全局影响,与特定进程或者连接无关。连接时间比较长时能复现,时间比较短时kill进程时能正常发FIN。看下conntrack相关的内核参数设置,发现ECS环境的conntrack参数中有一个显著的调整: net.netfilter.nf_conntrack_tcp_timeout_established = 120这个值默认值是5天,阿里云官网文档推荐的调优值是1200秒,而现在这个ECS环境中的设置是120秒,是一个非常短的值。 看到这里,可以认定是经过nf_conntrack_tcp_timeout_established 120秒后,conntrack中的连接跟踪记录已经被删除,此时对这个连接发起主动的FIN,在netfilter中回被判定成INVALID状态。而客户在iptables filter表的OUTPUT chain中对INVALID连接状态的报文采取的是drop行为,最终导致FIN报文在netfilter filter表OUTPUT chain中被丢弃。 FIN包被认为是INVALID状态?对于一个TCP连接,在conntrack中没有连接跟踪表项,一端FIN掉连接的时候的时候被认为是INVALID状态是很符合逻辑的事情。但是没有发现任何文档清楚地描述这个场景:当用户空间TCP socket仍然存在,但是conntrack表项已经不存在时,对一个“新”的报文,conntrack模块认为它是什么状态。 所有文档描述conntrack的NEW, ESTABLISHED, RELATED, INVALID状态时大同小异,比较详细的描述如文档: The NEW state tells us that the packet is the first packet that we see. This means that the first packet that the conntrack module sees, within a specific connection, will be matched. For example, if we see a SYN packet and it is the first packet in a connection that we see, it will match. However, the packet may as well not be a SYN packet and still be considered NEW. This may lead to certain problems in some instances, but it may also be extremely helpful when we need to pick up lost connections from other firewalls, or when a connection has already timed out, but in reality is not closed.如上对于NEW状态的描述为:conntrack module看见的一个报文就是NEW状态,例如TCP的SYN报文,有时候非SYN也被认为是NEW状态。 ...

June 5, 2019 · 4 min · jiezi

Nginx-转发-socket-端口配置

原文链接:何晓东 博客 Nginx 转发 socket 端口常见场景:在线学习应用,在常规功能之外,增加一个聊天室功能,后端选择 swoole 提供服务提供者,同时不想前端直接 ip:port 方式链接到服务,需要使用 Nginx 进行转发。常规情况,我们可以在用户页面,直接建立 socket 链接,但这样的操作会暴露端口,带来一定的安全隐患,使用 Nginx 进行转发,可以隐藏端口。额外的问题就是一些 header 参数也需要在转发过程中带给 socket 服务提供者,其他只需要 Nginx 处理一下从常规协议转换到 Websocket 就可以。 其中,"Upgrade" 是 逐跳(hop-by-hop) 头,无法从客户端转发到代理服务器,通过转发代理,客户端可以使用 CONNECT 方法来规避此问题。但是,这不适用于反向代理,因为客户端不知道任何代理服务器,并且需要在代理服务器上进行特殊处理。同时逐跳头包含 "Upgrade" 和 "Connection" 都无法传递,则需要在转换为 Websocket 的时候带上这两个参数:例如: location /chat/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";}进阶:让转发到代理服务器的 "Connection" 头字段的值,取决于客户端请求头的 "Upgrade" 字段值。例如: http { map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { ... location /chat/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } }注意:示例中的 http://backend 为一组负载均衡的服务器,只有单台服务器的,可以写成 proxy_pass http://127.0.0.1:9501; 这样的。 ...

June 3, 2019 · 1 min · jiezi

处理网络超时问题的最佳实践

对于云上的用户来说,业务日志里面报超时问题处理起来往往比价棘手,因为1) 问题点可能在云基础设施层,也有可能在业务软件层,需要排查的范围非常广;2) 这类问题往往是不可复现问题,抓到现场比较难。在本文里就分析下如何来分辨和排查这类问题的根本原因。 业务超时 != 网络丢包由于业务的形态不同,软件实现语言和框架的不同,业务日志中打印出的信息可能是各不相同,比如如下关键字: "SocketTimeOut", "Read timed out", "Request timeout" 等从形式看都属于网络超时这一类,但是需要明确一个概念:这类问题是发生的原因是请求超过了设定的timeout时间,这个设置有可能来自客户端,服务器端或者网络中间节点,这是直接原因。网络丢包可能会导致超时,但是并不是充分条件。总结业务超时和网络丢包的关系如下: 网络丢包可能造成业务超时,但是业务超时的原因不一定是丢包。明确了这个因果关系后,我们再来看怎么分析业务超时。如果武断地将业务超时等同于网络抖动丢包,那这个排查分析过程就完全错过了业务软件层本身的原因,很容易进入困局。 本文会从云基础设施层和业务软件层对业务超时做分析,总体来讲基础设置层面的丢包原因相对容易排查,阿里云有完善的底层监控,根据业务日志报错的对应时间段,从监控数据中可以确定是否有基础设施网络问题。而业务层的超时通常是软件层面的设置,和软件实现及业务形态都有关系,这种往往是更加难以排查的。 网络丢包为什么导致业务超时网络抖动可能造成业务超时,其主要原因是网络抖动会带来不同程度的延迟。本文以互联网大部分应用以来的TCP为对象来介绍,一个丢包对数据传输的完整性其实是没有影响的,因为TCP协议本身已经有精密的设计来处理丢包,乱序等异常情况。并且所有重传的处理都在内核TCP协议栈中完成,操作系统用户空间的进程对这个处理实际上是不感知的。丢包唯一的副作用的就是会增加延迟,如果这段延迟的时间足够长,达到了应用进程设置的某个Timeout时间,那么在业务应用侧表现出来的就是业务超时。 丢包出现时会不会发生超时,取决于应用进程的Timeout设置。比如数据传输中的只丢了一个TCP数据包,引发200 ms后的超时重传: 如果应用设置的Timeout为100 ms,TCP协议栈没有机会重传,应用就认为超时并关闭连接;如果应用设置的Timeout为500 ms,则TCP协议栈会完成重传,这个处理过程对应用进程透明。应用唯一的感知就是处理这次报文交互比基线处理时长多了200 ms,对于时间敏感度不是非常高的应用来说这个影响非常小。延迟到底有多大?在设置应用进程Timeout时间时有没有可以参考的定量值呢?虽然TCP中的RTT/RTO都是动态变化的,但TCP丢包的产生的影响可以做一定的定量总结。 对丢包产生的延迟主要有如下两类: TCP建连超时。如果网络抖动不幸丢掉了TCP的第一个建连SYN报文,对与不太老的内核版本来说,客户端会在1秒(Draft RFC 2988bis-02中定义)后重传SYN报文再次发起建连。1秒对于内网环境来说非常大,对于阿里云一个区域的机房来说,正常的RTT都是小个位数毫秒级别,1秒内如果没有丢包足够完成百个数据报的交互。TCP中间数据包丢包。TCP协议处理中间的数据丢包有快速重传和超时重传两种机制。快速重传通常比较快,和RTT相关,没有定量的值。超时重传 (RTO, Retrasmission Timeout) 也和RTT相关,但是Linux中定义的RTO的最小值为,TCP_RTO_MIN = 200ms。所以在RTT比较小的网络环境下,即使RTT小于1ms,TCP超时重传的RTO最小值也只能是200ms。这类丢包带来的延迟相对小。除了丢包,另外一类比较常见的延迟是TCP Delayed ACK带来的延迟。这个和协议设计相关,和丢包其实没有关系,在这里一并总结如延迟定量部分。在交互式数据流+Nagle算法的场景下比较容易触发。Linux中定义的Delayed ACK的最小值TCP_DELACK_MIN是40 ms。 所以总结下来有如下几个定量时间可以供参考: 40 ms, 在交互数据流中TCP Delayed ACK + Nagle算法开启的场景,最小delay值。200 ms,在RTT比较小的正常网络环境中,TCP数据包丢包,超时重传的最小值。1 s,较新内核版本TCP建立的第一个SYN包丢包时的重传时间,RFC2988bis中定义的initial RTO value TCP_TIMEOUT_INIT。3 s, 较老内核版本TCP建立的第一个SYN包丢包时的重传时间,RFC 1122中定义的initial RTO value TCP_TIMEOUT_INIT。云基础设施网络丢包基础设施网络丢包的原因可能有多种,主要总结如下3类: 云基础设施网络抖动 网络链路,物理网络设备,ECS/RDS等所在的宿主机虚拟化网络都有可能出现软硬件问题。云基础设施已经做了完备的冗余,来保证出现问题时能快速隔离,切换和恢复。 现象: 因为有网络冗余设备并可以快速恢复,这类问题通常表现为某单一时间点网络抖动,通常为秒级。抖动的具体现象是在那个时段新建连接失败,已建立的连接中断,在业务上可能表现为超时。影响面: 网络设备下通常挂很多主机,通常影响面比较大,比如同时影响多个ECS到RDS的连接。 云产品的限速丢包 很多网络云产品在售卖的时候有规格和带宽选项,比如ECS, SLB, NAT网关等。当云产品的流量或者连接数超过规格或者带宽限制时,也会出现丢包。这种丢包并非云厂商的故障,而是实际业务流量规模和选择云产品规格时的偏差所带来。这种问题通常从云产品提供的监控中就能分辨出来。 现象: 当流量或者连接数超过规格时,出现流量或者连接丢弃。问题可能间断并持续地出现,网络流量高峰期出现的概率更大。影响面: 通常只影响单一实例。但对于NAT网关做SNAT的场景,可能影响SNAT后的多个实例。 运营商网络问题在走公网的场景中,客户端和服务器之间的报文交互往往要经过多个AS (autonomous system)。若运营商中间链路出现问题往往会导致端到端丢包。 ...

May 14, 2019 · 1 min · jiezi

基于socket.io快速实现一个实时通讯应用

随着web技术的发展,使用场景和需求也越来越复杂,客户端不再满足于简单的请求得到状态的需求。实时通讯越来越多应用于各个领域。 HTTP是最常用的客户端与服务端的通信技术,但是HTTP通信只能由客户端发起,无法及时获取服务端的数据改变。只能依靠定期轮询来获取最新的状态。时效性无法保证,同时更多的请求也会增加服务器的负担。 WebSocket技术应运而生。 WebSocket概念不同于HTTP半双工协议,WebSocket是基于TCP 连接的全双工协议,支持客户端服务端双向通信。 WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。 实现原生实现WebSocket对象一共支持四个消息 onopen, onmessage, onclose和onerror。 建立连接通过javascript可以快速的建立一个WebSocket连接: var Socket = new WebSocket(url, [protocol] );以上代码中的第一个参数url, 指定连接的URL。第二个参数 protocol是可选的,指定了可接受的子协议。 同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。 当Browser和WebSocketServer连接成功后,会触发onopen消息。 Socket.onopen = function(evt) {};如果连接失败,发送、接收数据失败或者处理数据出现错误,browser会触发onerror消息。 Socket.onerror = function(evt) { };当Browser接收到WebSocketServer端发送的关闭连接请求时,就会触发onclose消息。 Socket.onclose = function(evt) { };收发消息当Browser接收到WebSocketServer发送过来的数据时,就会触发onmessage消息,参数evt中包含server传输过来的数据。 Socket.onmessage = function(evt) { };send用于向服务端发送消息。 Socket.send();socketWebSocket是跟随HTML5一同提出的,所以在兼容性上存在问题,这时一个非常好用的库就登场了——Socket.io。 socket.io封装了websocket,同时包含了其它的连接方式,你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也必须同样适用。 socket.io是基于 Websocket 的Client-Server 实时通信库。 socket.io底层是基于engine.io这个库。engine.io为 socket.io 提供跨浏览器/跨设备的双向通信的底层库。engine.io使用了 Websocket 和 XHR 方式封装了一套 socket 协议。在低版本的浏览器中,不支持Websocket,为了兼容使用长轮询(polling)替代。 API文档 Socket.io允许你触发或响应自定义的事件,除了connect,message,disconnect这些事件的名字不能使用之外,你可以触发任何自定义的事件名称。 ...

April 22, 2019 · 2 min · jiezi

Go Socket操作笔记

概念首先什么事Socket,翻译过来就是孔或者插座。网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。Socket的本质其实是编程接口,是一个IPC接口。(IPC:进程间通信)与其他IPC方法不同的是,它可以通过网络让多个进程建立通信,是的通信双方是否在同一个机器上变得无关紧要。Socket如何通信socket是通过TCP/IP协议族来提供网络链接。Socket是应用程序和运输层之间的抽象层,封装了TCP/IP协议族,用一组简单的接口就能就能通过网络链接通信。下图为网上经典图,用户不需要知道TCP/IP的各种复杂功能协议等,直接使用Socket提供的接口就能完成所有工作。Socket通信流程服务端: 首先服务端需要初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端:客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。Socket提供的主要接口初始化:int socket(int domain, int type, int protocol)绑定:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);监听:listen()接受请求:accept()具体参数的意义先不展开,我们主要是看go如何操作socket。Go 如何操作Socket上边简单的介绍了Socket的概念,在go语言中我们可以很方便的使用net包来操作。其实go的net包就是对上面的Socket的接口做了再次封装,让我们能很方便的建立Socket连接和使用连接通信。直接上代码公共函数//公共函数 用来定义Socket类型 ip 端口。const( Server_NetWorkType = “tcp” Server_Address = “127.0.0.1:8085” Delimiter = ‘\t’)// 往conn中写数据,可以用于客户端传输给服务端, 也可以服务端返回客户端func Write(conn net.Conn, content string)(int, error){ var buffer bytes.Buffer buffer.WriteString(content) buffer.WriteByte(Delimiter) return conn.Write(buffer.Bytes())}// 从conn中读取字节流,以上面的结束符为标记func Read(conn net.Conn)(string, error){ readBytes := make([]byte,1) var buffer bytes.Buffer for{ if _,err := conn.Read(readBytes);err != nil{ return “”, err } readByte := readBytes[0] if readByte == Delimiter{ break } buffer.WriteByte(readByte) } return buffer.String(), nil}服务端:func main() { // net listen 函数 传入socket类型和ip端口,返回监听对象 listener, err := net.Listen(socket.Server_NetWorkType,socket.Server_Address) if err == nil{ // 循环等待客户端访问 for{ conn,err := listener.Accept() if err == nil{ // 一旦有外部请求,并且没有错误 直接开启异步执行 go handleConn(conn) } } }else{ fmt.Println(“server error”, err) } defer listener.Close()}func handleConn(conn net.Conn){ for { // 设置读取超时时间 conn.SetReadDeadline(time.Now().Add(time.Second * 2)) // 调用公用方法read 获取客户端传过来的消息。 if str, err := socket.Read(conn); err == nil{ fmt.Println(“client:",conn.RemoteAddr(),str) // 通过write 方法往客户端传递一个消息 socket.Write(conn,“server got:"+str) } }}客户端func main() { // 调用net包中的dial 传入ip 端口 进行拨号连接,通过三次握手之后获取到conn conn,err := net.Dial(socket.Server_NetWorkType, socket.Server_Address) if err != nil{ fmt.Println(“Client create conn error err:”, err) } defer conn.Close() //往服务端传递消息 socket.Write(conn,“aaaa”) //读取服务端返回的消息 if str, err := socket.Read(conn);err == nil{ fmt.Println(str) }}可以看到,上边的代码很简单。使用net包就可以很轻松的实现Socket通信。简单源码查看我们可以看到最上边我们介绍的Socket最少需要有创建(socket函数) 绑定(bind函数)监听(listen函数)这些最基本的步骤,这些步骤其实都封装在我们的net包中,到了我们代码中客户从net.Listen 函数里查看源代码。因为代码调用过多只贴一些关键性代码段。首先 listen 会判断是监听tcp,还是unix。之后经过一些列的调用走到sysSocket方法,这个方法会调用系统的socket方法初始化socket对象返回一个socket的标识符。之后就会使用这个标识符进行绑定 监听。最终返回listener对象。func Listen(network, address string) (Listener, error) { addrs, err := DefaultResolver.resolveAddrList(context.Background(), “listen”, network, address, nil) if err != nil { return nil, &OpError{Op: “listen”, Net: network, Source: nil, Addr: nil, Err: err} } var l Listener switch la := addrs.first(isIPv4).(type) { case *TCPAddr: // 监听TCP l, err = ListenTCP(network, la) case *UnixAddr: l, err = ListenUnix(network, la) default: return nil, &OpError{Op: “listen”, Net: network, Source: nil, Addr: la, Err: &AddrError{Err: “unexpected address type”, Addr: address}} } if err != nil { return nil, err // l is non-nil interface containing nil pointer } return l, nil}// 最终调用的系统方法, 是不是跟socket 的初始化方法很像?func sysSocket(family, sotype, proto int) (int, error) { // See ../syscall/exec_unix.go for description of ForkLock. syscall.ForkLock.RLock() s, err := socketFunc(family, sotype, proto) if err == nil { syscall.CloseOnExec(s) } syscall.ForkLock.RUnlock() if err != nil { return -1, os.NewSyscallError(“socket”, err) } if err = syscall.SetNonblock(s, true); err != nil { poll.CloseFunc(s) return -1, os.NewSyscallError(“setnonblock”, err) } return s, nil} ...

March 30, 2019 · 2 min · jiezi

epoll 是如何工作的

本文包含以下内容:epoll是如何工作的本文不包含以下内容:epoll 的用法epoll 的缺陷我实在非常喜欢像epoll这样使用方便、原理不深却有大用处的东西,即使它可能已经比较老了select 和 poll 的缺点epoll 对于动辄需要处理上万连接的网络服务应用的意义可以说是革命性的。对于普通的本地应用,select 和 poll可能就很好用了,但对于像C10K这类高并发的网络场景,select 和 poll就捉襟见肘了。看看他们的APIint select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval timeout); int poll(struct pollfd fds, nfds_t nfds, int timeout);它们有一个共同点,用户需要将监控的文件描述符集合打包当做参数传入,每次调用时,这个集合都会从用户空间拷贝到内核空间,这么做的原因是内核对这个集合是无记忆的。对于绝大部分应用,这是一种十足的浪费,因为应用需要监控的描述符在大部分时间内基本都是不变的,也许会有变化,但都不大.epoll 对此的改进epoll对此的改进也正是它的实现方式,它需要完成以下两件事描述符添加—内核可以记下用户关心哪些文件的哪些事件.事件发生—内核可以记下哪些文件的哪些事件真正发生了,当用户前来获取时,能把结果提供给用户.描述符添加既然要有记忆,那么理所当然的内核需要需要一个数据结构来记, 这个数据结构简单点就像下面这个图中的epoll_instance, 它有一个链表头,链表上的元素epoll_item就是用户添加上去的, 每一项都记录了描述符fd和感兴趣的事件组合event事件发生事件有多种类型, 其中POLLIN表示的可读事件是用户使用的最多的。比如:当一个TCP的socket收到报文,它会变得可读;当一个pipe受到对端发送的数据,它会变得可读;当一个timerfd对应的定时器超时,它会变得可读;那么现在需要将这些可读事件和前面的epoll_instance关联起来。linux中,每一个文件描述符在内核都有一个struct file结构对应, 这个struct file有一个private_data指针,根据文件的实际类型,它们指向不同的数据结构。那么我能想到的最方便的做法就是epoll_item中增加一个指向struct file的指针,在struct file中增加一个指回epoll item的指针。为了能记录有事件发生的文件,我们还需要在epoll_instance中增加一个就绪链表readylist,在private_data指针指向的各种数据结构中增加一个指针回指到 struct file,在epoll item中增加一个挂接点字段,当一个文件可读时,就把它对应的epoll item挂接到epoll_instance在这之后,用户通过系统调用下来读取readylist就可以知道哪些文件就绪了。好了,以上纯属我个人一拍脑袋想到的epoll大概的工作方式,其中一定包含不少缺陷。不过真实的epoll的实现思想上与上面也差不多,下面来说一下创建 epoll 实例如同上面的epoll_instance,内核需要一个数据结构保存记录用户的注册项,这个结构在内核中就是struct eventpoll, 当用户使用epoll_create(2)或者epoll_create1(2)时,内核fs/eventpoll.c实际就会创建一个这样的结构./ * Create the internal data structure (“struct eventpoll”). /error = ep_alloc(&ep);这个结构中比较重要的部分就是几个链表了,不过实例刚创建时它们都是空的,后续可以看到它们的作用epoll_create()最终会向用户返回一个文件描述符,用来方便用户之后操作该 epoll实例,所以在创建epoll实例之后,内核就会分配一个文件描述符fd和对应的struct file结构/ Creates all the items needed to setup an eventpoll file. That is, a file structure and a free file descriptor./fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));最后就是把它们和刚才的epoll实例 关联起来,然后向用户返回fdep->file = file;fd_install(fd, file);return fd;完成后,epoll实例 就成这样了。向 epoll 实例添加一个文件描述符用户可以通过 epoll_ctl(2)向 epoll实例 添加要监控的描述符和感兴趣的事件。如同前面的epoll item,内核实际创建的是一个叫struct epitem的结构作为注册表项。如下图所示为了在描述符很多时的也能有较高的搜索效率, epoll实例 以红黑树的形式来组织每个struct epitem (取代上面例子中链表)。struct epitem结构中ffd是用来记录关联文件的字段, 同时它也作为该表项添加到红黑树上的Key;rdllink的作用是当fd对应的文件准备好(关心的事件发生)时,内核会将它作为挂载点挂接到epoll实例中ep->rdllist链表上fllink的作用是作为挂载点挂接到fd对应的文件的file->f_tfile_llink链表上,一般这个链表最多只有一个元素,除非发生了dup。pwqlist是一个链表头,用来连接 poll wait queue。虽然它是链表,但其实链表上最多只会再挂接一个元素。创建struct epitem的代码在fs/evnetpoll.c的ep_insert()中if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL))) return -ENOMEM;之后会进行各个字段初始化/ Item initialization follow here … /INIT_LIST_HEAD(&epi->rdllink);INIT_LIST_HEAD(&epi->fllink);INIT_LIST_HEAD(&epi->pwqlist);epi->ep = ep;ep_set_ffd(&epi->ffd, tfile, fd);epi->event = event;epi->nwait = 0;epi->next = EP_UNACTIVE_PTR;然后是设置局部变量epqstruct ep_pqueue epq;epq.epi = epi;init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);epq的数据结构是struct ep_pqueue,它是poll table的一层包装(加了一个struct epitem 的指针)struct ep_pqueue{ poll_table pt; struct epitem epi;}poll table包含一个函数和一个事件掩码typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct );typedef struct poll_table_struct { poll_queue_proc _qproc; unsigned long _key; // store the interested event masks}poll_table;这个poll table用在哪里呢 ? 答案是,用在了struct file_operations的poll操作 (这和本文开始说的selectpoll不是一个东西)struct file_operations { // code omitted… unsigned int (poll)(struct file, struct poll_table_struct); // code omitted…}不同的文件有不同poll实现方式, 但一般它们的实现方式差不多是下面这种形式static unsigned int XXXX_poll(struct file *file, poll_table *wait){ 私有数据 = file->private_data; unsigned int events = 0; poll_wait(file, &私有数据->wqh, wait); if (文件可读了) events |= POLLIN; return events;} 它们主要实现两个功能将XXX放到文件私有数据的等待队列上 (一般file->private_data中都有一个等待队列头wait_queue_head_t wqh), 至于XXX是啥,各种类型文件实现各异,取决于poll_table参数查询是否真的有事件了,若有则返回.有兴趣的读者可以 timerfd_poll() 或者 pipe_poll() 它们的实现poll_wait的实现很简单, 就是调用poll_table中设置的函数, 将文件私有的等待队列当作了参数.static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table p){ if (p && p->_qproc && wait_address) p->_qproc(filp, wait_address, p);} 回到 ep_insert()所以这里设置的poll_table就是ep_ptable_queue_proc().然后revents = ep_item_poll(epi, &epq.pt); 看其实现可以看到,其实就是主动去调用文件的poll函数. 这里以TCP socket文件为例好了(毕竟网络应用是最广泛的)/ * ep_item_poll -> sock_poll -> tcp_poll */unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait) { sock_poll_wait(file, sk_sleep(sk), wait); // will call poll_wait() // code omitted…}可以看到,最终还是调用到了poll_wait(),所以注册的ep_ptable_queue_proc()会执行 struct epitem *epi = ep_item_from_epqueue(pt); struct eppoll_entry *pwq; pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)这里面, 又分配了一个struct eppoll_entry结构. 其实它和struct epitem 结构是一一对应的.随后就是一些初始化 init_waitqueue_func_entry(&pwq->wait, ep_poll_callback); // set func:ep_poll_callback pwq->whead = whead; pwq->base = epi; add_wait_queue(whead, &pwq->wait) list_add_tail(&pwq->llink, &epi->pwqlist); epi->nwait++; 这其中比较重要的是设置pwd->wait.func = ep_poll_callback。现在, struct epitem 和struct eppoll_entry的关系就像下面这样文件可读之后对于TCP socket, 当收到对端报文后,最初设置的sk->sk_data_ready函数将被调用void sock_init_data(struct socket *sock, struct sock *sk){ // code omitted… sk->sk_data_ready = sock_def_readable; // code omitted…}经过层层调用,最终会调用到 __wake_up_common 这里面会遍历挂在socket.wq上的等待队列上的函数static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key){ wait_queue_t *curr, *next; list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !–nr_exclusive) break; }} 于是, 顺着图中的这条红色轨迹, 就会调用到我们设置的ep_poll_callback,那么接下来就是要让epoll实例能够知有文件已经可读了先从入参中取出当前表项epi和ep struct epitem *epi = ep_item_from_wait(wait); struct eventpoll ep = epi->ep; 再把epi挂到ep的就绪队列if (!ep_is_linked(&epi->rdllink)) { list_add_tail(&epi->rdllink, &ep->rdllist) } 接着唤醒阻塞在(如果有)该epoll实例的用户.waitqueue_active(&ep->wq) 用户获取事件谁有可能阻塞在epoll实例的等待队列上呢? 当然就是使用epoll_wait来从epoll实例获取发生了感兴趣事件的的描述符的用户.epoll_wait会调用到ep_poll()函数.if (!ep_events_available(ep)) { / * We don’t have any available event to return to the caller. * We need to sleep here, and we will be wake up by * ep_poll_callback() when events will become available. */ init_waitqueue_entry(&wait, current); __add_wait_queue_exclusive(&ep->wq, &wait); 如果没有事件,我们就将自己挂在epoll实例的等待队列上然后睡去…..如果有事件,那么我们就要将事件返回给用户ep_send_events(ep, events, maxevents) 参考资料the-implementation-of-epoll ...

March 15, 2019 · 3 min · jiezi

java nio中,为什么客户端一方正常关闭了Socket,而服务端的isReadable()还总是返回true?

我这篇文章想讲的是编程时如何正确关闭tcp连接。首先给出一个网络上绝大部分的java nio代码示例:服务端:1首先实例化一个多路I/O复用器Selector2然后实例化一个ServerSocketChannel3ServerSocketChannel注册为非阻塞(channel.configureBlocking(false);)4ServerSocketChannel注册到Selector,并监听连接事件(serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);)5Selector开始轮询,如果监听到了isAcceptable()事件,就建立一个连接,如果监听到了isReadable()事件,就读数据。6处理完或者在处理每个事件之前将SelectionKey移除出Selector.selectedKeys()代码:package qiuqi.main;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.*;import java.util.Iterator;public class NioServer { public static void main(String[] args) throws IOException { startServer(); } static void startServer() throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(999)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() > 0) { Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey sk = iterator.next(); iterator.remove(); if (sk.isAcceptable()) { SocketChannel channel = serverSocketChannel.accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); } else if (sk.isReadable()) { System.out.println(“读事件!!!”); SocketChannel channel = (SocketChannel) sk.channel(); try { ByteBuffer byteBuffer = ByteBuffer.allocate(200); //这里只读数据,未作任何处理 channel.read(byteBuffer); } catch (IOException e) { //手动关闭channel System.out.println(e.getMessage()); sk.cancel(); if (channel != null) channel.close(); } } } } }}还有说明一下,为什么在if (sk.isReadable()){}这个里面加上异常捕捉,因为可能读数据的时候客户端突然断掉,如果不捕捉这个异常,将会导致整个程序结束。而客户端如果使用NIO编程,那么和服务端很像,然鹅,我们并不需要使用NIO编程,因为这里我想讲的问题和NIO或是普通IO无关,在我想讲的问题上,他俩是一样的,那么我就用普通socket编程来讲解,因为这个好写:)。直接给代码如下:package qiuqi.main;import java.io.IOException;import java.net.InetSocketAddress;import java.net.Socket;public class TraditionalSocketClient { public static void main(String[] args) throws IOException { startClient(); } static void startClient() throws IOException { Socket socket = new Socket(); socket.connect(new InetSocketAddress(999)); socket.getOutputStream().write(new byte[100]); //要注意这个close方法,这是正常关闭socket的方法 //也是导致这个错误的根源 socket.close(); }}我们运行客户端和服务端的代码,输出的结果是:读事件!!!读事件!!!读事件!!!读事件!!!读事件!!!读事件!!!….读事件!!!读事件!!!无限个读事件!!!why???客户端正常关闭,然后显然客户端不可能再给服务端发送任何数据了,服务端怎么可能还有读响应呢?我们现在把客户端代码的最后一行socket.close();这个去掉,再运行一次!输出结果是:读事件!!!读事件!!!远程主机强迫关闭了一个现有的连接。然后。。。就正常了(当然代码里会有异常提示的),这里的正常指的是不会输出多余的读事件!!!了。这又是怎么回事?我们知道如果去掉socket.close();那么客户端是非正常关闭,服务端这边会引发IOException。引发完IOExpection之后,我们的程序在catch{}语句块中手动关闭了channel。既然非正常关闭会引发异常,那么正常关闭呢?什么都不引发?但是这样服务端怎么知道客户端已经关闭了呢?显然服务端会收到客户端的关闭信号(可读数据),而网络上绝大多数代码并没有根据这个关闭信号来结束channel。那么关闭信号是什么?channel.read(byteBuffer);这个语句是有返回值的,大多数情况是返回一个大于等于0的值,表示将多少数据读入byteBuffer缓冲区。然鹅,当客户端正常断开连接的时候,它就会返回-1。虽然这个断开连接信号也是可读数据(会使得isReadable()为true),但是这个信号无法被读入byteBuffer,也就是说一旦返回-1,那么无论再继续读多少次都是-1,并且会引发可读事件isReadable()。因此,这样写问题就能得到解决,下面的代码在try语句块里。 SocketChannel channel = (SocketChannel) sk.channel(); try { ByteBuffer byteBuffer = ByteBuffer.allocate(200); int num; //这里只读数据,未作任何处理 num = channel.read(byteBuffer); if(num == -1) throw new IOException(“读完成”); } catch (IOException e) { System.out.println(e.getMessage()); sk.cancel(); if (channel != null) channel.close(); }这里我根据返回值-1来抛出异常,使得下面的catch语句块捕捉并关闭连接,也可以不抛出异常,直接在try{}里处理。 ...

March 4, 2019 · 1 min · jiezi

C++回声服务器_6-多进程pipe版本

在服务器多进程版本的基础上,使用管道来向一个子进程发送接收到的数据,该子进程将接收到的数据保存到文件中。客户端代码不变。服务器代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <csignal>#include <sys/wait.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 30;void error_handling(const char messgae);void read_childproc(int sig);// 接收一个参数,argv[1]为端口号int main(int argc, char argcv[]) { int server_sock, client_sock; struct sockaddr_in server_addr, client_addr; int fds[2]; // 管道描述符 pid_t pid; struct sigaction act; socklen_t addr_size; int str_len, state; char buf[BUF_SIZE]; if (argc != 2) { printf(“Usgae : %s <port>\n”, argcv[0]); exit(1); } act.sa_handler = read_childproc; sigemptyset(&act.sa_mask); act.sa_flags = 0; state = sigaction(SIGCHLD, &act, 0); server_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(atoi(argcv[1])); if (bind(server_sock, (struct sockaddr)&server_addr, sizeof(server_addr)) == -1) { error_handling(“bind() error”); } if (listen(server_sock, 5) == -1) { error_handling(“listen() error”); } pipe(fds); pid = fork(); if (pid == 0) { // 负责保存数据到文件的子进程 FILE fp = fopen(“echomsg.txt”, “wt”); char msgbuf[BUF_SIZE]; int len; for (int i = 0; i < 10; ++i) { len = read(fds[0], msgbuf, BUF_SIZE); fwrite((void)msgbuf, 1, len, fp); } fclose(fp); return 0; } while (1) { addr_size = sizeof(client_addr); client_sock =accept(server_sock, (struct sockaddr)&server_addr, &addr_size); if (client_sock == -1) { continue; } else { puts(“new client connected…”); } pid = fork(); if (pid == 0) { close(server_sock); while ((str_len = read(client_sock, buf, BUF_SIZE)) != 0) { write(client_sock, buf, str_len); write(fds[1], buf, str_len); // 发送数据给负责保存文件的子进程 } close(client_sock); puts(“client disconnected…”); return 0; } else { close(client_sock); } } close(server_sock); return 0;}项目代码github参考《TCP/IP网络编程》 ...

February 27, 2019 · 2 min · jiezi

C++回声服务器_4-UDP connect版本

针对UDP套接字调用connect函数不会与对方UDP套接字建立连接,只是向UDP套接字注册目标IP和端口信息。修改客户端代码服务器代码不需要修改,只需修改客户端代码。调用connect函数之后,可以调用write函数和read函数来发送、接收数据,而不需要调用sendto函数和recvfrom函数。include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 30;void error_handling(const char *message);// 接收两个参数,argv[1]为IP地址,argv[2]为端口号int main(int argc, char *argv[]) { int sock; char message[BUF_SIZE]; ssize_t str_len; struct sockaddr_in server_addr; if (argc != 3) { printf(“Usage : %s <IP> <port>\n”, argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { error_handling(“socket() error”); } // 地址信息初始化 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPV4 地址族 server_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器IP地址 server_addr.sin_port = htons(atoi(argv[2])); // 服务器端口号 while (1) { fputs(“Insert message(q or Q to quit): “, stdout); fgets(message, BUF_SIZE, stdin); // 如果输入q或者Q,则退出 if (!strcmp(message, “q\n”) || !strcmp(message, “Q\n”)) { break; } write(sock, message, strlen(message)); str_len = read(sock, message, sizeof(message) - 1); message[str_len] = 0; printf(“Message from server: %s”, message); } close(sock); return 0;}项目代码github参考《TCP/IP网络编程》 ...

February 27, 2019 · 1 min · jiezi

C++回声服务器_5-多进程版本

服务器和客户端都是用多进程来接收和发送数据。服务器代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <csignal>#include <sys/wait.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 30;void error_handling(const char messgae);void read_childproc(int sig);// 接收一个参数,argv[1]为端口号int main(int argc, char argcv[]) { int server_sock, client_sock; struct sockaddr_in server_addr, client_addr; pid_t pid; struct sigaction act; socklen_t addr_size; int str_len, state; char buf[BUF_SIZE]; if (argc != 2) { printf(“Usgae : %s <port>\n”, argcv[0]); exit(1); } act.sa_handler = read_childproc; sigemptyset(&act.sa_mask); act.sa_flags = 0; state = sigaction(SIGCHLD, &act, 0); server_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(atoi(argcv[1])); if (bind(server_sock, (struct sockaddr)&server_addr, sizeof(server_addr)) == -1) { error_handling(“bind() error”); } if (listen(server_sock, 5) == -1) { error_handling(“listen() error”); } while (1) { addr_size = sizeof(client_addr); client_sock =accept(server_sock, (struct sockaddr)&server_addr, &addr_size); if (client_sock == -1) { continue; } else { puts(“new client connected…”); } pid = fork(); if (pid == 0) { close(server_sock); while ((str_len = read(client_sock, buf, BUF_SIZE)) != 0) { write(client_sock, buf, str_len); } close(client_sock); puts(“client disconnected…”); return 0; } else { close(client_sock); } } close(server_sock); return 0;}客户端代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 30;void error_handling(const char *messgae);void read_routine(int sock, char *buf);void write_routine(int sock, char *buf);// 接收两个参数,argv[1]为IP地址,argv[2]为端口号int main(int argc, char argv[]) { int sock; pid_t pid; char buf[BUF_SIZE]; struct sockaddr_in server_addr; if (argc != 3) { printf(“Usage : %s <IP> <port>\n”, argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(argv[1]); server_addr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr)&server_addr, sizeof(server_addr)) == -1) { error_handling(“connect() error”); } pid = fork(); if (pid == 0) { write_routine(sock, buf); } else { read_routine(sock, buf); } close(sock); return 0;}辅助函数#include <cstdio>#include <cstdlib>#include <csignal>#include <cstring>#include <unistd.h>#include <sys/wait.h>#include <sys/socket.h>const int BUF_SIZE = 30;// 处理错误void error_handling(const char *message) { printf("%s", message); exit(1);}// 读取进程退出状态void read_childproc(int sig) { pid_t pid; int status; pid = waitpid(-1, &status, WNOHANG); printf(“removed proc id: %d\n”, pid);}// 客户端接收数据void read_routine(int sock, char *buf) { while (1) { ssize_t str_len = read(sock, buf, BUF_SIZE); if (str_len == 0) { return; } buf[str_len] = 0; printf(“Message from server: %s”, buf); }}// 客户端发送数据void write_routine(int sock, char *buf) { while (1) { fgets(buf, BUF_SIZE, stdin); if (!strcmp(buf, “q\n”) || !strcmp(buf, “Q\n”)) { shutdown(sock, SHUT_WR); return; } write(sock, buf, strlen(buf)); }}项目代码github参考《TCP/IP网络编程》 ...

February 27, 2019 · 2 min · jiezi

(一)如何实现一个单进程阻塞的网络服务器

概述想要更好的理解,网络编程,写出一个高性能的服务,我们需要花点时间来理解下对于服务器处理客户端的整个流程并且理解一些关键的术语,本来想在本文中补充一些基础理论知识,担心篇幅过长不利于阅读,所以以后补发一些基础知识,接下来进入正题。理论主要介绍下实现一个网络服务器的基本步骤,代码会在实践环节复现一次。第一步我们需要创建一个socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤。第二步进入while循环,阻塞在accept操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程。accept函数返回客户端连接的socket第三步利用fread读取客户端socket当中的数据收到数据后服务器程序进行处理然后使用fwrite向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会close。实践在这里我们用代码来实现下基本一个流程,在开始写代码之前介绍介几个php函数,是我们代码中可能会用到的,方便大家理解。函数stream_socket_serverstream_socket_acceptcall_user_funcis_callablefread点击函数了解用法代码废话少说直接开撸~<?php class Worker{ //监听socket protected $socket = NULL; //连接事件回调 public $onConnect = NULL; //接收消息事件回调 public $onMessage = NULL; public function __construct($socket_address) { } public function run(){ } }$worker = new Worker(’tcp://0.0.0.0:9810’);//提前注册了一个连接事件回调$worker->onConnect = function ($data) { echo ‘新的连接来了’, $data, PHP_EOL;};//提前注册了一个接收消息事件回调$worker->onMessage = function ($conn, $message) {};$worker->run();按照之前的流程我们需要监听端口+地址public function __construct($socket_address) { //监听地址+端口 $this->socket=stream_socket_server($socket_address); }下一步就需要阻塞在accept操作,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程public function run(){ while (true) { //循环监听 $client = stream_socket_accept($this->socket);//在服务端阻塞监听 } }当新的连接进入唤醒进程并且触发连接事件回调 public function run(){ while (true) { //循环监听 $client = stream_socket_accept($this->socket);//在服务端阻塞监听 if(!empty($client) && is_callable($this->onConnect)){//socket连接成功并且是我们的回调 //触发事件的连接的回调 call_user_func($this->onConnect,$client); } } }这里的连接回调实际上触发的就是之前准备好类库的这里下面这段代码$worker->onConnect = function ($data) { echo ‘连接事件:’, $data, PHP_EOL;};当连接成功后利用fread获取到客户端的内容,并触发接收消息事件 public function run(){ while (true) { //循环监听 $client = stream_socket_accept($this->socket);//在服务端阻塞监听 if(!empty($client) && is_callable($this->onConnect)){//socket连接成功并且是我们的回调 //触发事件的连接的回调 call_user_func($this->onConnect,$client); } //从连接中读取客户端内容 $buffer=fread($client,65535);//参数2:在缓冲区当中读取的最大字节数 //正常读取到数据。触发消息接收事件,进行响应 if(!empty($buffer) && is_callable($this->onMessage)){ //触发时间的消息接收事件 call_user_func($this->onMessage,$this,$client,$buffer);//传递到接收消息事件》当前对象、当前连接、接收到的消息 } } }到此处基本的一个网络服务接收基本完成,还需要对请求做出一个响应,以HTTP请求为例,这里封装了一个http响应的方法(http://127.0.0.1:9810) class Worker{ … … … public function send($conn,$content){ $http_resonse = “HTTP/1.1 200 OK\r\n”; $http_resonse .= “Content-Type: text/html;charset=UTF-8\r\n”; $http_resonse .= “Connection: keep-alive\r\n”; $http_resonse .= “Server: php socket server\r\n”; $http_resonse .= “Content-length: “.strlen($content)."\r\n\r\n”; $http_resonse .= $content; fwrite($conn, $http_resonse); } }当触发接收消息事件时对http请求做出响应$worker->onMessage = function ($server,$conn, $message) { echo ‘来自客户端消息:’,$message,PHP_EOL; $server->send($conn,‘来自服务端消息’);};到这就结束了~,完整代码直通车缺点一次只能处理一个连接,不支持多个连接同时处理 ...

February 27, 2019 · 1 min · jiezi

C++回声服务器_3-UDP版本

这次我们实现一个UDP版本的回声服务器。用于传输数据的函数UDP套接字不会像TCP套接字那样保持连接状态,因此每次传输数据都要添加目标地址信息。用于传输数据的函数:发送数据到目标服务器。#include <sys/socket.h>ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); 其中to为存有目标服务器地址信息的sockaddr结构体变量的地址值。接收来自服务器的数据。#include <sys/socket.h>ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t addrlen); 其中from为存有发送端地址信息的sockaddr结构体变量的地址值服务器代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 30;void error_handling(const char message);// 接收一个参数,argv[0]为端口号int main(int argc, char argv[]) { int server_socket; char message[BUF_SIZE]; ssize_t str_len; socklen_t client_addr_size; int i; struct sockaddr_in server_addr; struct sockaddr_in client_addr; if (argc != 2) { printf(“Usage: %s <port>\n”, argv[0]); exit(1); } server_socket = socket(PF_INET, SOCK_DGRAM, 0); // 创建IPv4 TCP socket if (server_socket == -1) { error_handling(“UDP socket create error”); } // 地址信息初始化 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPV4 地址族 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 使用INADDR_ANY分配服务器的IP地址 server_addr.sin_port = htons(atoi(argv[1])); // 端口号由第一个参数设置 // 分配地址信息 if (bind(server_socket, (struct sockaddr)&server_addr, sizeof(sockaddr)) == -1) { error_handling(“bind() error”); } while (1) { client_addr_size = sizeof(client_addr); // 读取来自客户端的数据 str_len = recvfrom(server_socket, message, BUF_SIZE, 0, (struct sockaddr)&client_addr, &client_addr_size); // 发送数据给客户端 sendto(server_socket, message, str_len, 0, (struct sockaddr)&client_addr, client_addr_size); } printf(“echo server\n”); return 0;}注:while循环内没有break语句,因此是无限循环,close函数不会执行。客户端代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 30;void error_handling(const char message);// 接收两个参数,argv[0]为IP地址,argv[1]为端口号int main(int argc, char argv[]) { int sock; char message[BUF_SIZE]; ssize_t str_len; socklen_t addr_size; struct sockaddr_in server_addr, from_addr; if (argc != 3) { printf(“Usage : %s <IP> <port>\n”, argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { error_handling(“socket() error”); } // 地址信息初始化 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPV4 地址族 server_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器IP地址 server_addr.sin_port = htons(atoi(argv[2])); // 服务器端口号 while (1) { fputs(“Insert message(q or Q to quit): “, stdout); fgets(message, BUF_SIZE, stdin); // 如果输入q或者Q,则退出 if (!strcmp(message, “q\n”) || !strcmp(message, “Q\n”)) { break; } sendto(sock, message, strlen(message), 0, (struct sockaddr)&server_addr, sizeof(sockaddr)); // 发送数据到服务器 addr_size = sizeof(from_addr); str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr)&from_addr, &addr_size); // 接收数据 message[str_len] = 0; printf(“Message from server: %s”, message); } close(sock); return 0;}UDP地址分配UDP地址分配应在sendto函数调用前完成:调用bind函数。如果调用sendto函数是发现尚未分配地址信息,则在首次调用sendto函数时给相应的套接字自动分配IP和端口。项目代码github参考《TCP/IP网络编程》 ...

February 26, 2019 · 2 min · jiezi

【Linux系统编程】普通用户绑定(bind)特权端口

有些知识不常使用真的容易忘啊,即使没有忘记,知识提取速度也够下午茶的。背景最近在学Haskell,今天用Haskell的Network.Socket模块实现了一个简单的基于TCP的daytime服务程序。程序运行阶段报了以下的错误:Network.Socket.bind: permission denied (Permission denied)我的第一反应怀疑是不是本地有服务程序占用端口号13,然后用命令netstat -tunl | grep 13查看,端口号并没有占用,所以第一种可能性不成立。是不是这个模块有类似的bug呢?但并没有查到。不放心,用C语言写了同样功能的程序,然后运行也会出错:bind error: Permission denied那是不是端口的问题,隐隐约约记得好像是小于1024的端口号不是预留给用户的。然后换了一个>=1024的端口,果然运行成功了。至此,思路才走上正轨。实际上小于1024的端口是特权端口,普通用户是没有权限绑定的。那如果我就是要用端口13呢,怎么解决呢?解决方案1.使用root权限运行最简单的方式就是登录root帐号,或者使用su切换到root。如果本机有配置sudo,普通用户也可使用该命令运行服务程序。2.使用setcap给服务程序赋予能力上一种方式是使用具有特权的用户,而该种方式是给程序赋予绑定特权端口的能力。操作如下:使用setcap ‘CAP_NET_BIND_SERVICE=+ep’ /path/to/program赋予(raise)绑定特权端口的能力使用setcap ‘CAP_NET_BIND_SERVICE=-ep’ /path/to/program清除(lower)绑定特权端口的能力也可使用setcap -r /path/to/program清除(remove)该程序的所有能力setcap简单使用说明除了CAP_NET_BIND_SERVICE,还有好多能力,可以参考capabilities(7)=、-是运算符,除此之外还有+;e和p是标记,除此之外还有i。可参考cap_from_text(3) Textual Representation一节的说明。setcap命令对内核有要求,必须>=2.6.24。 请关注我的公众号哦。

February 25, 2019 · 1 min · jiezi

TCP socket和web socket的区别

小编先习惯性的看了下某中文百科网站对Web Socket的介绍,觉得很囧。如果大家按照这个答案去参加BAT等互联网公司的前端开发面试,估计会被鄙视。还是让我们阅读一些英文材料吧。让我们直接看stackoverflow上的原文,然后翻译:原文地址:https://stackoverflow.com/que…这个讨论有超过8万的阅读量。首先我们来阅读这段有166个赞的回答:When you send bytes from a buffer with a normal TCP socket, the send function returns the number of bytes of the buffer that were sent. 当我们向一个通常的TCP套接字发送一段来自内存buffer中的字节数据时,send系统调用返回的是实际发送的字节数。If it is a non-blocking socket or a non-blocking send then the number of bytes sent may be less than the size of the buffer. 如果发送数据的目的方套接字是一个非阻塞套接字或者是对写操作非阻塞的套接字,那么send返回的已发送字节数可能小于buffer中待发送字节数。If it is a blocking socket or blocking send, then the number returned will match the size of the buffer but the call may block. 如果是阻塞套接字,两者会相等,因为顾名思义,如果send系统调用没有把所有待发送数据全部发送,则API调用不会返回。With WebSockets, the data that is passed to the send method is always either sent as a whole “message” or not at all. Also, browser WebSocket implementations do not block on the send call.而Web socket和TCP socket的区别,从发送的数据来看,不再是一系列字节,而是按照一个完整的"消息体"发送出去的,这个"消息体"无法进一步再分割,要么全部发送成功,要么压根就不发送,不存在像TCP套接字非阻塞操作那样出现部分发送的情况。换言之,Web Socket里对套接字的操作是非阻塞操作。这个区别在维基百科上也有清晰阐述:Websocket differs from TCP in that it enables a stream of messages instead of a stream of bytes再来看接收方的区别。原文:But there are more important differences on the receiving side of things. When the receiver does a recv (or read) on a TCP socket, there is no guarantee that the number of bytes returned correspond to a single send (or write) on the sender side. It might be the same, it may be less (or zero) and it might even be more (in which case bytes from multiple send/writes are received). With WebSockets, the receipt of a message is event driven (you generally register a message handler routine), and the data in the event is always the entire message that the other side sent.同理,在TCP套接字的场景下,接收方从TCP套接字读取的字节数,并不一定等于发送方调用send所发送的字节数。而WebSocket呢?WebSocket的接收方从套接字读取数据,根本不是像TCP 套接字那样直接用recv/read来读取, 而是采取事件驱动机制。即应用程序注册一个事件处理函数,当web socket的发送方发送的数据在接收方应用从内核缓冲区拷贝到应用程序层已经处于可用状态时 ,应用程序注册的事件处理函数以回调(callback)的方式被调用。看个例子:我通过WebSocket发送一个消息“汪子熙”:在调试器里看到的这个字符串作为回调函数的输入参数注入到函数体内:Chrome开发者工具里观察到的WebSocket消息体:下次面试被面试官问到TCP和WebSocket套接字的区别,相信大家应该能够知道如何回答了。要获取更多Jerry的原创文章,请关注公众号"汪子熙": ...

February 24, 2019 · 2 min · jiezi

C++回声服务器_2-修复客户端问题

C++回声服务器_1-简单版本中的问题出在客户端。客户端通过write函数一次性发送数据,过一段时间再调用一次read函数,期望接收传输的数据。问题在于这段时间到底是多久?理想的客户端应在接收到数据时立即读取数据。改进客户端发送数据时,可以知道数据的大小(长度)。客户端接收数据的时候,可以知道接收到数据的大小(长度)。所以,客户端循环调用read函数,直到接收到的数据总大小(长度)等于发送的数据的大小(长度)时,已完成所有数据的接收。修改的代码:// 发送的字符串长度、接收字符串的长度、每次read函数接受到字符串的长度ssize_t str_len, recv_len, recv_cnt;str_len = write(sock, message, strlen(message)); // 向服务器发送数据recv_len = 0;// 循环调用read函数,直到接收到所有数据为止while (recv_len < str_len) { recv_cnt = read(sock, message, BUF_SIZE); // 读取来自客户端的服务器 if (recv_cnt == -1) { error_handling(“read() error”); } recv_len += recv_cnt;}message[recv_len] = 0;完整的客户端代码:#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 1024;void error_handling(const char *message);// 接收两个参数,argv[0]为IP地址,argv[1]为端口号int main(int argc, char argv[]) { int sock; struct sockaddr_in server_addr; char message[BUF_SIZE]; // 发送的字符串长度、接收字符串的长度、每次read函数接受到字符串的长度 ssize_t str_len, recv_len, recv_cnt; if (argc != 3) { printf(“Usage : %s <IP> <port>\n”, argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling(“socket() error”); } // 地址信息初始化 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPV4 地址族 server_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器IP地址 server_addr.sin_port = htons(atoi(argv[2])); // 服务器端口号 // 向服务器发送连接请求 if (connect(sock, (struct sockaddr)&server_addr, sizeof(server_addr)) == -1) { error_handling(“connect() error”); } else { printf(“Connect…”); } while (1) { printf(“Input message( Q to quit ): “); fgets(message, BUF_SIZE, stdin); // 如果输入q或者Q,则退出 if (!strcmp(message, “q\n”) || !strcmp(message, “Q\n”)) { break; } str_len = write(sock, message, strlen(message)); // 向服务器发送数据 recv_len = 0; // 循环调用read函数,直到接收到所有数据为止 while (recv_len < str_len) { recv_cnt = read(sock, message, BUF_SIZE); // 读取来自客户端的服务器 if (recv_cnt == -1) { error_handling(“read() error”); } recv_len += recv_cnt; } message[recv_len] = 0; printf(“Message from server: %s \n”, message); } // 关闭连接 close(sock); return 0;}项目代码github参考《TCP/IP网络编程》 ...

February 24, 2019 · 1 min · jiezi

[C++回声服务器_1]简单版本

C++网络编程离不开socket编程。我们现在使用socket编写简单的回声服务器。流程这里所说的流程包括两部分:socket函数调用流程。服务器与客户端交互流程。socket函数调用流程服务器与客户端交互流程回声服务器主要功能:服务器将接收到来自客户端的数据传回客户端。服务器的功能:服务器在同一时刻只能与一个客户端相连,并提供回声服务。服务器依次向5个客户端提供回声服务并退出。客户端的功能:客户端接收终端的数据并发送到服务器。客户端接收到Q或者q时,客户端断开连接并退出。服务器代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 1024;void error_handling(const char message);// 接收两个参数,argv[0]为端口号int main(int argc, char argv[]) { int server_socket; int client_sock; char message[BUF_SIZE]; ssize_t str_len; int i; struct sockaddr_in server_addr; struct sockaddr_in client_addr; socklen_t client_addr_size; if (argc != 2) { printf(“Usage: %s <port>\n”, argv[0]); exit(1); } server_socket = socket(PF_INET, SOCK_STREAM, 0); // 创建IPv4 TCP socket if (server_socket == -1) { error_handling(“socket() error”); } // 地址信息初始化 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPV4 地址族 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 使用INADDR_ANY分配服务器的IP地址 server_addr.sin_port = htons(atoi(argv[1])); // 端口号由第一个参数设置 // 分配地址信息 if (bind(server_socket, (struct sockaddr)&server_addr, sizeof(sockaddr)) == -1) { error_handling(“bind() error”); } // 监听连接请求,最大同时连接数为5 if (listen(server_socket, 5) == -1) { error_handling(“listen() error”); } client_addr_size = sizeof(client_addr); for (i = 0; i < 5; ++i) { // 受理客户端连接请求 client_sock = accept(server_socket, (struct sockaddr)&client_addr, &client_addr_size); if (client_sock == -1) { error_handling(“accept() error”); } else { printf(“Connect client %d\n”, i + 1); } // 读取来自客户端的数据 while ((str_len = read(client_sock, message, BUF_SIZE)) != 0) { // 向客户端传输数据 write(client_sock, message, (size_t)str_len); message[str_len] = ‘\0’; printf(“client %d: message %s”, i + 1, message); } } // 关闭连接 close(client_sock); printf(“echo server\n”); return 0;}客户端代码#include <cstdio>#include <cstdlib>#include <cstring>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>const int BUF_SIZE = 1024;void error_handling(const char *message);// 接收两个参数,argv[0]为IP地址,argv[1]为端口号int main(int argc, char argv[]) { int sock; struct sockaddr_in server_addr; char message[BUF_SIZE]; ssize_t str_len; if (argc != 3) { printf(“Usage : %s <IP> <port>\n”, argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling(“socket() error”); } // 地址信息初始化 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPV4 地址族 server_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器IP地址 server_addr.sin_port = htons(atoi(argv[2])); // 服务器端口号 // 向服务器发送连接请求 if (connect(sock, (struct sockaddr)&server_addr, sizeof(server_addr)) == -1) { error_handling(“connect() error”); } else { printf(“Connect…”); } while (1) { printf(“Input message( Q to quit ): “); fgets(message, BUF_SIZE, stdin); // 如果输入q或者Q,则退出 if (!strcmp(message, “q\n”) || !strcmp(message, “Q\n”)) { break; } write(sock, message, strlen(message)); // 向服务器发送数据 str_len = read(sock, message, BUF_SIZE); // 读取来自客户端的服务器 message[str_len] = 0; printf(“Message from server: %s \n”, message); } // 关闭连接 close(sock); return 0;}不足客户端传输数据,通过调用write函数一次性发送,之后调用一次read函数,期望接收自己传输的数据。只是问题所在。因为“TCP不存在数据边界”,存在两个异常情况:客户端多次调用write函数传输的数据有可能一次性发送给服务器。服务器调用一次write函数传输数据,但数据太大,操作系统有可能把数据分成多个数据包发送到客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用了read函数。参考《TCP/IP网络编程》 ...

February 23, 2019 · 2 min · jiezi

完成端口服务器模型

前提:IOCP的整体编程模型跟上面的纯重叠io 非常类似. 纯重叠io使用OVERLAPPED + APC函数完成.这种模型的缺点是必须让调用apc函数进入alterable状态. 而IOCP解决了这个问题.IOCP让我们自己创建一些线程,然后调用GetQueuedCompletionStatus 来告诉我们某个io操作完成, 就像是在另一个线程中执行了APC函数一样; 使用IOCP 的时候,一般情况下需要自己创建额外的线程,用于等待结果完成(GetQueuedCompletionStatus)使用到的函数:CreateIoCompletionPort : 创建/ 关联一个完成端口 . 第3个参数是一个自定义数据, 第4个是最多N个线程可被调用; 注意与其关联的HANDLE 必须要有OVERLAPPED属性的//创建一个完成端口HANDLE hComp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0) //关联到完成端口. 第3个参数是一个自定义数据//在GetQueuedCompletionStatus将携带这些数据返回. 这个自定义数据将一直与此套接字绑定在了一起CreateIoCompletionPort((HANDLE)client_socket, hComp, (DWORD)pSockData, 0);GetQueuedCompletionStatus :一旦类似WSARecv / WSASend 完成后 . 用此函数获取结果,就想APC函数一样,一旦完成io操作就调用. 此函数一般情况都在某一个线程中使用.注意一旦在某个线程中调用了此函数,这意味着,该线程就像被指派给了IOCP一样,供IOCP使用. 总之这个行为就想APC函数在另一个线程被调用了; 关于解除关联: 一旦一个套接字关闭了 , closehandle /closesocket. 就将从IOCP的设备句柄列表中解除关联了 关于线程:CreateIoCompletionPort 最后一个参数用于指定IOCP最多执行N个线程(如果是0 则使用默认CPU的核数). 但一般情况下,我会预留一些额外的线程.比如我的CPU是4核即IOCP最多可使用 4个线程 , 不过一般情况下会创建 8 个线程,给IOCP预留 额外4个线程 . 原因是如果IOCP有5个任务已经完成, 最多只有4个线程被唤醒. 如果其中某个线程调用了WaitForSingleObject 之类的函数 ,此时IOCP将唤醒额外的线程来处理第5个任务; 先补充一下. 对于WSARecv / WSASend 的OVERLAPPED操作,简称为投递操作.意思是让操作系统去干活,至于什么时候干完.GetQueuedCompletionStatus 会通知你(即返回) . 因此因此, 需要注意, 这些参数像WSABUF 和 OVERLAPPED 一定要 new / malloc在堆中;代码中都有注释: 另代码中有很多返回都没判断.这个例子仅仅解释如何编写IOCP #include “stdafx.h”#include <process.h>#include “../utils.h” //包含了一些宏和一些打印错误信息的函数. #define BUFFSIZE 8192#define Read 0#define Write 1 //自定义数据 . 注意 结构的地址 与 第一个成员的地址相同struct IOData{ WSAOVERLAPPED overlapped; //每个io操作都需要独立的一个overlapped WSABUF wsabuf; //读写各一份 int rw_mode; //判断读写操作 char * buf; //真正存放数据的地方, 需要初始化}; //自定义数据.保存客户套接字和地址struct SocketData{ SOCKET hClientSocket; //客户端套接字 SOCKADDR_IN clientAddr; IOData * pRead; //2个指针,只是为了在线程中方便使用添加的 IOData * pWrite;}; //用于交换2个bufint swapBuf(WSABUF * a, WSABUF * b){ BOOL ret = FALSE; if (a && b){ char * buf = a->buf; a->buf = b->buf; b->buf = buf; ret = TRUE; } return ret;} //释放内存,解除关联void freeMem(SocketData * pSockData){ closesocket(pSockData->hClientSocket); free(pSockData->pRead->buf); free(pSockData->pWrite->buf); free(pSockData->pWrite); free(pSockData->pRead); free(pSockData);} unsigned int WINAPI completeRoutine(void * param){ //完成端口 HANDLE hCom = (HANDLE)param; SocketData * pSockData = NULL; IOData * pIOData = NULL; DWORD flags = 0, bytes = 0; BOOL ret = 0; SOCKET hClientSocket = NULL; printf(“tid:%ld start!\n”, GetCurrentThreadId()); while (1) { flags = 0; //直到有任务完成即返回 ret = GetQueuedCompletionStatus(hCom, &bytes, (PULONG_PTR)&pSockData, (LPOVERLAPPED * )&pIOData, INFINITE); printf(“GetQueuedCompletionStatus : %d , diy key : %p , pIOData:%p,mode:%d\n”, ret, pSockData, pIOData,pIOData->rw_mode); //如果成功了 if (ret) { hClientSocket = pSockData->hClientSocket; //如果是WSARecv的 if (Read == pIOData->rw_mode) { printf(“READ - > bytesRecved:%ld, high:%ld\n”, bytes, pIOData->overlapped.InternalHigh); //对端关闭 if (0 == bytes) { printf(“peer closed\n”); freeMem(pSockData); //释放内存 continue; } //测试数据 pSockData->pRead->buf[bytes] = 0; printf(“Read buf:%s\n”, pSockData->pRead->buf); //交换指针, 把recv的buf 给 write的buf; //把write的buf交换给recv . 如果并发量不大的时候可以这么做 swapBuf(&pIOData->wsabuf, &pSockData->pWrite->wsabuf); //回传操作.清空write OVERLAPPED memset(&pSockData->pWrite->overlapped, 0, sizeof(WSAOVERLAPPED)); pSockData->pWrite->wsabuf.len = bytes; WSASend(hClientSocket, &pSockData->pWrite->wsabuf, 1, NULL, 0, &pSockData->pWrite->overlapped, NULL); //再次投递一个recv操作,等待下次客户端发送 memset(&pSockData->pRead->overlapped, 0, sizeof(WSAOVERLAPPED)); pSockData->pRead->wsabuf.len = BUFFSIZE; WSARecv(hClientSocket, &pSockData->pRead->wsabuf, 1, NULL, &flags, &pSockData->pRead->overlapped, NULL); } else { // send 完成. printf(“Send finsished - > bytes:%ld, high:%ld\n”, bytes, pIOData->overlapped.InternalHigh); memset(&pIOData->overlapped, 0, sizeof(WSAOVERLAPPED)); } } else{ //一旦出错, 解除绑定即删除内存 print_error(GetLastError()); freeMem(pSockData); } } return 0;}int _tmain(int argc, _TCHAR* argv[]){ WSADATA wsadata; if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0){ print_error(WSAGetLastError()); return 0; } SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); //指定线程数量. 一般 processors * 2 const DWORD nThreads = sysinfo.dwNumberOfProcessors * 2; //创建一个完成端口 , 前3个参数保证了创建一个独立的完成端口, 最后一个参数指定了完成 //端口可使用的线程数. 0 使用当前cpu核数 HANDLE hCom = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); //准备一些线程供完成端口调用, 把完成端口同时传入 HANDLE * arr_threads = new HANDLE[nThreads]; for (int i = 0; i < sysinfo.dwNumberOfProcessors; ++i) arr_threads[i] = (HANDLE)_beginthreadex(NULL, 0, completeRoutine, (void*)hCom, 0, NULL); //创建一个支持OVERLAPPED的socket.这样的属性将被 accept 返回的socket所继承 SOCKET hListenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN serv_addr, client_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); serv_addr.sin_addr.s_addr = INADDR_ANY; bind(hListenSocket, (SOCKADDR*)&serv_addr, sizeof(serv_addr)); listen(hListenSocket, BACKLOG); SOCKET client_socket; int client_addr_size = 0; DWORD flags = 0; while (1){ client_addr_size = sizeof(client_addr); flags = 0; client_socket = accept(hListenSocket, (SOCKADDR*)&client_addr, &client_addr_size); puts(“accepted”); //准备一份数据, 用于保存clientsocket, addr, 以及读写指针; SocketData * pSockData = (SocketData *)malloc(sizeof(SocketData)); pSockData->pRead = NULL; pSockData->pWrite = NULL; pSockData->hClientSocket = client_socket; memcpy(&pSockData->clientAddr, &client_addr, client_addr_size); //准备数据 IOData * pRead = (IOData *)malloc(sizeof(IOData)); //对于OVERLAPPED,需要额外注意,清0 memset(&pRead->overlapped, 0, sizeof(WSAOVERLAPPED)); pRead->buf = (char *)malloc(BUFFSIZE); pRead->rw_mode = Read; pRead->wsabuf.buf = pRead->buf; pRead->wsabuf.len = BUFFSIZE; pSockData->pRead = pRead; IOData *pWrite = (IOData *)malloc(sizeof(IOData)); pWrite->buf = (char *)malloc(BUFFSIZE); memset(&pWrite->overlapped, 0, sizeof(WSAOVERLAPPED)); pWrite->rw_mode = Write; pWrite->wsabuf.buf = pWrite->buf; pWrite->wsabuf.len = BUFFSIZE; pSockData->pWrite = pWrite; //与iocp关联在一起. 注意第3个参数, 把自定义数据一起传递过去 CreateIoCompletionPort((HANDLE)client_socket, hCom, (DWORD)pSockData, 0); WSARecv(client_socket, &pRead->wsabuf, 1, NULL, &flags, &pRead->overlapped, NULL); } return 0;} ...

January 17, 2019 · 3 min · jiezi

通过FD耗尽实验谈谈使用HttpClient的正确姿势

一段问题代码实验在进行网络编程时,正确关闭资源是一件很重要的事。在高并发场景下,未正常关闭的资源数逐渐积累会导致系统资源耗尽,影响系统整体服务能力,但是这件重要的事情往往又容易被忽视。我们进行一个简单的实验,使用HttpClient-3.x编写一个demo请求指定的url,看看如果不正确关闭资源会发生什么事。public String doGetAsString(String url) { GetMethod getMethod = null; String is = null; InputStreamReader inputStreamReader = null; BufferedReader br = null; try { HttpClient httpclient = new HttpClient();//问题标记① getMethod = new GetMethod(url); httpclient.executeMethod(getMethod); if (HttpStatus.SC_OK == getMethod.getStatusCode()) { ……//对返回结果进行消费,代码省略 } return is; } catch (Exception e) { if (getMethod != null) { getMethod.releaseConnection(); //问题标记② } } finally { inputStreamReader.close(); br.close(); ……//关闭流时的异常处理代码省略 } return null; }这段代码逻辑很简单, 先创建一个HttpClient对象,用url构建一个GetMethod对象,然后发起请求。但是用这段代码并发地以极高的QPS去访问外部的url,很快就会在日志中看到“打开文件太多,无法打开文件”的错误,后续的http请求都会失败。这时我们用lsof -p &dollar;{javapid}命令去查看java进程打开的文件数,发现达到了655350这么多。分析上面的代码片段,发现存在以下2个问题:(1)初始化方式不对。标记①直接使用new HttpClient()的方式来创建HttpClient,没有显示指定HttpClient connection manager,则构造函数内部默认会使用SimpleHttpConnectionManager,而SimpleHttpConnectionManager的默认参数中alwaysClose的值为false,意味着即使调用了releaseConnection方法,连接也不会真的关闭。(2)在未使用连接池复用连接的情况下,代码没有正确调用releaseConnection。catch块中的标记②是唯一调用了releaseConnection方法的代码,而这段代码仅在发生异常时才会走到,大部分情况下都走不到这里,所以即使我们前面用正确的方式初始化了HttpClient,由于没有手动释放连接,也还是会出现连接堆积的问题。可能有同学会有以下疑问:1、明明是发起Http请求,为什么会打开这么多文件呢?为什么是655350这个上限呢?2、正确的HttpClient使用姿势是什么样的呢?这就涉及到linux系统中fd的概念。什么是fd在linux系统中有“一切皆文件”的概念。打开和创建普通文件、Socket(套接字)、Pipeline(管道)等,在linux内核层面都需要新建一个文件描述符来进行状态跟踪和使用。我们使用HttpClient发起请求,其底层需要首先通过系统内核创建一个Socket连接,相应地就需要打开一个fd。为什么我们的应用最多只能创建655350个fd呢?这个值是如何控制的,能否调整呢?事实上,linux系统对打开文件数有多个层面的限制:1)限制单个Shell进程以及其派生子进程能打开的fd数量。用ulimit命令能查看到这个值。2)限制每个user能打开的文件总数。具体调整方法是修改/etc/security/limits.conf文件,比如下图中的红框部分就是限制了userA用户只能打开65535个文件,userB用户只能打开655350个文件。由于我们的应用在服务器上是以userB身份运行的,自然就受到这里的限制,不允许打开多于655350个文件。# /etc/security/limits.conf##<domain> <type> <item> <value>userA - nofile 65535userB - nofile 655350# End of file3)系统层面允许打开的最大文件数限制,可以通过“cat /proc/sys/fs/file-max”查看。前文demo代码中错误的HttpClient使用方式导致连接使用完成后没有成功断开,连接长时间保持CLOSE_WAIT状态,则fd需要继续指向这个套接字信息,无法被回收,进而出现了本文开头的故障。再识HttpClient我们的代码中错误使用common-httpclient-3.x导致后续请求失败,那这里的common-httpclient-3.x到底是什么东西呢?相信所有接触过网络编程的同学对HttpClient都不会陌生,由于java.net中对于http访问只提供相对比较低级别的封装,使用起来很不方便,所以HttpClient作为Jakarta Commons的一个子项目出现在公众面前,为开发者提供了更友好的发起http连接的方式。然而目前进入Jakarta Commons HttpClient官网,会发现页面最顶部的“End of life”栏目,提示此项目已经停止维护了,它的功能已经被Apache HttpComponents的HttpClient和HttpCore所取代。同为Apache基金会的项目,Apache HttpComponents提供了更多优秀特性,它总共由3个模块构成:HttpComponents Core、HttpComponents Client、HttpComponents AsyncClient,分别提供底层核心网络访问能力、同步连接接口、异步连接接口。在大多数情况下我们使用的都是HttpComponents Client。为了与旧版的Commons HttpClient做区分,新版的HttpComponents Client版本号从4.x开始命名。从源码上来看,Jakarta Commons HttpClient和Apache HttpComponents Client虽然有很多同名类,但是两者之间没有任何关系。以最常使用到的HttpClient类为例,在commons-httpclient中它是一个类,可以直接发起请求;而在4.x版的httpClient中,它是一个接口,需要使用它的实现类。既然3.x与4.x的HttpClient是两个完全独立的体系,那么我们就分别讨论它们的正确用法。HttpClient 3.x用法回顾引发故障的那段代码,通过直接new HttpClient()的方式创建HttpClient对象,然后发起请求,问题出在了这个构造函数上。由于我们使用的是无参构造函数,查看三方包源码,会发现内部会通过无参构造函数new一个SimpleHttpConnectionManager,它的成员变量alwaysClose在不特别指定的情况下默认为false。alwaysClose这个值是如何影响到我们关闭连接的动作呢?继续跟踪下去,发现HttpMethodBase(它的多个实现类分别对应HTTP中的几种方法,我们最常用的是GetMethod和PostMethod)中的releaseConnection()方法首先会尝试关闭响应输入流(下图中的①所指代码),然后在finally中调用ensureConnectionRelease(),这个方法内部其实是调用了HttpConnection类的releaseConnection()方法,如下图中的标记③所示,它又会调用到SimpleHttpConnectionManager的releaseConnection(conn)方法,来到了最关键的标记④和⑤。标记④的代码说明,如果alwaysClose=true,则会调用httpConnection.close()方法,它的内部会把输入流、输出流都关闭,然后把socket连接关闭,如标记⑥和⑦所示。然后,如果标记④处的alwaysClose=false,则会走到⑤的逻辑中,调用finishLastResponse()方法,如标记⑧所示,这段逻辑实际上只是把请求响应的输入流关闭了而已。我们的问题代码就是走到了这段逻辑,导致没能把之前使用过的连接断开,而后续的请求又没有复用这个httpClient,每次都是new一个新的,导致大量连接处于CLOSE_WAIT状态占用系统文件句柄。通过以上分析,我们知道使用commons-httpclient-3.x之后如果想要正确关闭连接,就需要指定always=true且正确调用method.releaseConnection()方法。上述提到的几个类,他们的依赖关系如下图(红色箭头标出的是我们刚才讨论到的几个类):其中SimpleHttpConnectionManager这个类的成员变量和方法列表如下图所示:事实上,通过对commons-httpclient-3.x其他部分源码的分析,可以得知还有其他方法也可以正确关闭连接。方法1:先调用method.releaseConnection(),然后获取到httpClient对象的SimpleHttpConnectionManager成员变量,主动调用它的shutdown()方法即可。对应的三方包源码如下图所示,其内部会调用httpConnection.close()方法。方法2:先调用method.releaseConnection(),然后获取到httpClient对象的SimpleHttpConnectionManager成员变量,主动调用closeIdleConnections(0)即可,对应的三方包源码如下。方法3:由于我们使用的是HTTP/1.1协议,默认会使用长连接,所以会出现上面的连接不释放的问题。如果客户端与服务端双方协商好不使用长连接,不就可以解决问题了吗。commons-httpclient-3.x也确实提供了这个支持,从下面的注释也可以看出来。具体这样操作,我们在创建了method后使用method.setRequestHeader(“Connection”, “close”)设置头部信息,并在使用完成后调用一次method.releaseConnection()。Http服务端在看到此头部后会在response的头部中也带上“Connection: close”,如此一来httpClient发现返回的头部有这个信息,则会在处理完响应后自动关闭连接。HttpClient 4.x用法既然官方已经不再维护3.x,而是推荐所有使用者都升级到4.x上来,我们就顺应时代潮流,重点看看4.x的用法。(1)简易用法最简单的用法类似于3.x,调用三方包提供的工具类静态方法创建一个CloseableHttpClient对象,然后发起调用,如下图。这种方式创建的CloseableHttpClient,默认使用的是PoolingHttpClientConnectionManager来管理连接。由于CloseableHttpClient是线程安全的,因此不需要每次调用时都重新生成一个,可以定义成static字段在多线程间复用。如上图,我们在获取到response对象后,自己决定如何处理返回数据。HttpClient的三方包中已经为我们提供了EntityUtils这个工具类,如果使用这个类的toString()或consume()方法,则上图finally块红框中的respnose.close()就不是必须的了,因为EntityUtils的方法内部会在处理完数据后把底层流关闭。(2)简易用法涉及到的核心类详解CloseableHttpClient是一个抽象类,我们通过HttpClients.createDefault()创建的实际是它的子类InternalHttpClient。/** * Internal class. * * @since 4.3 /@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)@SuppressWarnings(“deprecation”)class InternalHttpClient extends CloseableHttpClient implements Configurable { … …}继续跟踪httpclient.execute()方法,发现其内部会调用CloseableHttpClient.doExecute()方法,实际会调到InternalHttpClient类的doExecute()方法。通过对请求对象(HttpGet、HttpPost等)进行一番包装后,最后实际由execChain.execute()来真正执行请求,这里的execChain是接口ClientExecChain的一个实例。接口ClientExecChain有多个实现类,由于我们使用HttpClients.createDefault()这个默认方法构造了CloseableHttpClient,没有指定ClientExecChain接口的具体实现类,所以系统默认会使用RedirectExec这个实现类。/* * Base implementation of {@link HttpClient} that also implements {@link Closeable}. * * @since 4.3 /@Contract(threading = ThreadingBehavior.SAFE)public abstract class CloseableHttpClient implements HttpClient, Closeable { private final Log log = LogFactory.getLog(getClass()); protected abstract CloseableHttpResponse doExecute(HttpHost target, HttpRequest request, HttpContext context) throws IOException, ClientProtocolException; … …}RedirectExec类的execute()方法较长,下图进行了简化。可以看到如果远端返回结果标识需要重定向(响应头部是301、302、303、307等重定向标识),则HttpClient默认会自动帮我们做重定向,且每次重定向的返回流都会自动关闭。如果中途发生了异常,也会帮我们把流关闭。直到拿到最终真正的业务返回结果后,直接把整个response向外返回,这一步没有帮我们关闭流。因此,外层的业务代码在使用完response后,需要自行关闭流。执行execute()方法后返回的response是一个CloseableHttpResponse实例,它的实现是什么?点开看看,这是一个接口,此接口唯一的实现类是HttpResponseProxy。/* * Extended version of the {@link HttpResponse} interface that also extends {@link Closeable}. * * @since 4.3 /public interface CloseableHttpResponse extends HttpResponse, Closeable {}我们前面经常看到的response.close(),实际是调用了HttpResponseProxy的close()方法,其内部逻辑如下:/* * A proxy class for {@link org.apache.http.HttpResponse} that can be used to release client connection * associated with the original response. * * @since 4.3 / class HttpResponseProxy implements CloseableHttpResponse { @Override public void close() throws IOException { if (this.connHolder != null) { this.connHolder.close(); } } … …}/* * Internal connection holder. * * @since 4.3 /@Contract(threading = ThreadingBehavior.SAFE)class ConnectionHolder implements ConnectionReleaseTrigger, Cancellable, Closeable { … … @Override public void close() throws IOException { releaseConnection(false); }}可以看到最终会调用到ConnectionHolder类的releaseConnection(reusable)方法,由于ConnectionHolder的close()方法调用releaseConnection()时默认传入了false,因此会走到else的逻辑中。这段逻辑首先调用managedConn.close()方法,然后调用manager.releaseConnection()方法。managedConn.close()方法实际是把连接池中已经建立的连接在socket层面断开连接,断开之前会把inbuffer清空,并把outbuffer数据全部传送出去,然后把连接池中的连接记录也删除。manager.releaseConnection()对应的代码是PoolingHttpClientConnectionManager.releaseConnection(),这段代码代码本来的作用是把处于open状态的连接的socket超时时间设置为0,然后把连接从leased集合中删除,如果连接可复用则把此连接加入到available链表的头部,如果不可复用则直接把连接关闭。由于前面传入的reusable已经强制为false,因此实际关闭连接的操作已经由managedConn.close()方法做完了,走到PoolingHttpClientConnectionManager.releaseConnection()中真正的工作基本就是清除连接池中的句柄而已。如果想了解关闭socket的细节,可以通过HttpClientConnection.close()继续往下跟踪,最终会看到真正关闭socket的代码在BHttpConnectionBase中。/* * This class serves as a base for all {@link HttpConnection} implementations and provides * functionality common to both client and server HTTP connections. * * @since 4.0 /public class BHttpConnectionBase implements HttpConnection, HttpInetConnection { … … @Override public void close() throws IOException { final Socket socket = this.socketHolder.getAndSet(null); if (socket != null) { try { this.inbuffer.clear(); this.outbuffer.flush(); try { try { socket.shutdownOutput(); } catch (final IOException ignore) { } try { socket.shutdownInput(); } catch (final IOException ignore) { } } catch (final UnsupportedOperationException ignore) { // if one isn’t supported, the other one isn’t either } } finally { socket.close(); } } } … …}为什么说调用了EntityUtils的部分方法后,就不需要再显示地关闭流呢?看下它的源码就明白了。/* * Static helpers for dealing with {@link HttpEntity}s. * * @since 4.0 /public final class EntityUtils { /* * Ensures that the entity content is fully consumed and the content stream, if exists, * is closed. * * @param entity the entity to consume. * @throws IOException if an error occurs reading the input stream * * @since 4.1 */ public static void consume(final HttpEntity entity) throws IOException { if (entity == null) { return; } if (entity.isStreaming()) { final InputStream instream = entity.getContent(); if (instream != null) { instream.close(); } } } … …}(3)HttpClient进阶用法在高并发场景下,使用连接池有效复用已经建立的连接是非常必要的。如果每次http请求都重新建立连接,那么底层的socket连接每次通过3次握手创建和4次握手断开连接将是一笔非常大的时间开销。要合理使用连接池,首先就要做好PoolingHttpClientConnectionManager的初始化。如下图,我们设置maxTotal=200且defaultMaxPerRoute=20。maxTotal=200指整个连接池中连接数上限为200个;defaultMaxPerRoute用来指定每个路由的最大并发数,比如我们设置成20,意味着虽然我们整个池子中有200个连接,但是连接到"http://www.taobao.com"时同一时间最多只能使用20个连接,其他的180个就算全闲着也不能给发到"http://www.taobao.com"的请求使用。因此,对于高并发的场景,需要合理分配这2个参数,一方面能够防止全局连接数过多耗尽系统资源,另一方面通过限制单路由的并发上限能够避免单一业务故障影响其他业务。private static volatile CloseableHttpClient instance; static { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); // Increase max total connection to 200 cm.setMaxTotal(200); // Increase default max connection per route to 20 cm.setDefaultMaxPerRoute(20); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(1000) .setSocketTimeout(1000) .setConnectionRequestTimeout(1000) .build(); instance = HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(requestConfig) .build(); }官方同时建议我们在后台起一个定时清理无效连接的线程,因为某些连接建立后可能由于服务端单方面断开连接导致一个不可用的连接一直占用着资源,而HttpClient框架又不能百分之百保证检测到这种异常连接并做清理,因此需要自给自足,按照如下方式写一个空闲连接清理线程在后台运行。public class IdleConnectionMonitorThread extends Thread { private final HttpClientConnectionManager connMgr; private volatile boolean shutdown; Logger logger = LoggerFactory.getLogger(IdleConnectionMonitorThread.class); public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) { super(); this.connMgr = connMgr; } @Override public void run() { try { while (!shutdown) { synchronized (this) { wait(5000); // Close expired connections connMgr.closeExpiredConnections(); // Optionally, close connections // that have been idle longer than 30 sec connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } } } catch (InterruptedException ex) { logger.error(“unknown exception”, ex); // terminate } } public void shutdown() { shutdown = true; synchronized (this) { notifyAll(); } }}我们讨论到的几个核心类的依赖关系如下:HttpClient作为大家常用的工具,看似简单,但是其中却有很多隐藏的细节值得探索。本文作者:闲鱼技术-峰明阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 14, 2019 · 3 min · jiezi

异步io通知 WSAEventSelect

WSAEventSelect 就是 select的增强版;注意WSAEventSelect 是通知异步, 而不是传送数据异步;总的来说就是一个异步的阻塞模型;如果要与 select 做个比较的话 :select 在 需要进行或者可以进行io处理时 返回. 而WSAEventSelect 在返回时(WSAWaitForMultipleEvents) 与io状态无关;例如: select 在收到数据 并返回时 , 他监听此套接字并检查接受缓冲区, 等到缓冲区能读时再返回.而WSAEventSelect 的等待函数 WSAWaitForMultipleEvents 只要此套接字有事件发生就返回; 因此称为异步通知;另WSAEventSelect 与 nix下的 epoll 整体编程模型很像; epoll : epoll说明具体使用到的函数:WSACreateEvent 创建一个事件, 默认手动模式WSAEventSelect 让一个套接字与一个事件捆绑在一起 注册到操作系统, 无需像select 每次重置;WSAWaitForMultipleEvents (阻塞) 等待套接字对应的事件发生, 最多能监听WSA_MAXIMUM_WAIT_EVENTS 个事件;WSAEnumNetworkEvents 查看套接字对应的具体事件 ; 这也就是与select 返回时机的不同的原因; 注意:此函数将重置事件,因此无需调用WSAResetEvent;重要就这4个函数, 其他的函数调用查看msdn 即可;echo_serv.c#include “../utils.h”#define BUFF_SIZE 8192 //关闭套接字后 . 调整数组static void adjust_sockarr(SOCKET * sock_arr, int start_index, int total){ for (int i = start_index; i < total; ++i) sock_arr[i] = sock_arr[i + 1];} //关闭事件后, 调整数组static void adjust_eventarr(WSAEVENT * event_arr, int start_index, int total){ for (int i = start_index; i < total; ++i) event_arr[i] = event_arr[i + 1];} int _tmain(int argc, _TCHAR* argv[]){ unsigned short port = 0; scanf(" %hd", &port); WSADATA wsadata; WSAStartup(MAKEWORD(2,2),&wsadata); SOCKET listensock = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN serv_addr, cli_addr; memset(&serv_addr, 0, sizeof(serv_addr)); memset(&cli_addr, 0, sizeof(cli_addr)); serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); if (bind(listensock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR){ print_error(WSAGetLastError()); return 0; } if (listen(listensock, 5) == SOCKET_ERROR){ print_error(WSAGetLastError()); return 0; } //创建一个与监听套接字捆绑的事件 WSAEVENT wsaevent = WSACreateEvent(); //FD_ACCEPT :一旦连接发生, 此事件将发生 if (WSAEventSelect(listensock, wsaevent, FD_ACCEPT) == SOCKET_ERROR){ print_error(WSAGetLastError()); return 0; } int event_count = 0 , cli_len = sizeof(cli_addr); //准备2个数组,用于存放套接字与事件 SOCKET sock_arr[WSA_MAXIMUM_WAIT_EVENTS] = {0}; WSAEVENT event_arr[WSA_MAXIMUM_WAIT_EVENTS] = {0}; //把套接字与事件存放在数组里 , 并一一对应 sock_arr[event_count] = listensock; event_arr[event_count] = wsaevent; ++event_count; // 事件总数 == 套接字总数 DWORD pos , start_index ,event_index , strlen; WSANETWORKEVENTS netevents; SOCKET cli_socket; char buf[BUFF_SIZE]; while (1){ //等待事件发生,只要一个事件发生就返回, 具体参数查看msdn; pos = WSAWaitForMultipleEvents(event_count, event_arr, FALSE, WSA_INFINITE, FALSE); printf(“pos:%d , event_count:%d\n”, pos,event_count); start_index = pos - WSA_WAIT_EVENT_0; //循环的意义:在一个繁忙的服务器上有可能在一个事件发生的瞬间,又有一个事件发生了 for (int i = start_index; i < event_count; ++i){ //依次验证从这个已经返回的事件,以及之后的套接字事件有没有发生. //由于timeout参数是 0 ,所以非阻塞 event_index = WSAWaitForMultipleEvents(1, event_arr+i, TRUE, 0, FALSE); if (WSA_WAIT_FAILED == event_index || WSA_WAIT_TIMEOUT == event_index){ printf(“index:%d , timeout or failed\n”, i); continue; } //有事件发生了,查看套接字对应的具体事件是什么 if (WSAEnumNetworkEvents(sock_arr[i], event_arr[i], &netevents) == SOCKET_ERROR){ _tprintf(TEXT(“WSAEnumNetworkEvents error : index:%d \ndetail:”), i); print_error(WSAGetLastError()); continue; } strlen = sprintf(buf, “sock_arr[%d] occurs “, i); buf[strlen] = 0; if (netevents.lNetworkEvents & FD_CONNECT) strcat(buf, " connect “); if (netevents.lNetworkEvents & FD_ACCEPT) strcat(buf , " accept “); if (netevents.lNetworkEvents & FD_WRITE) strcat(buf , " write “); if (netevents.lNetworkEvents & FD_CLOSE) strcat(buf , " close “); puts(buf); //如果有连接 if (netevents.lNetworkEvents & FD_ACCEPT){ //如果有错 if (netevents.iErrorCode[FD_ACCEPT_BIT] != 0){ _tprintf(TEXT(“accept error , index:%d,detail:\n”),i); print_error(WSAGetLastError()); continue; } cli_len = sizeof(cli_addr); cli_socket = accept(sock_arr[i], (SOCKADDR*)&cli_addr, &cli_len); //给新的连接创建事件 wsaevent = WSACreateEvent(); //把套接子与事件注册进操作系统,用于监听 if (WSAEventSelect(cli_socket, wsaevent, FD_READ | FD_CLOSE) != SOCKET_ERROR){ //放进数组 sock_arr[event_count] = cli_socket; event_arr[event_count] = wsaevent; ++event_count; printf(“new client ip:%s, port:%d\n”, inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); } } //如果套接字能读 if (netevents.lNetworkEvents & FD_READ){ if (netevents.iErrorCode[FD_READ_BIT] != 0){ _tprintf(TEXT(“read error , index:%d detail:\n”), i); print_error(WSAGetLastError()); continue; } strlen = recv(sock_arr[i], buf, BUFF_SIZE, 0); buf[strlen] = 0; strlen = send(sock_arr[i], buf, strlen, 0); printf(“recv len : %d, buf:%s\n”, strlen, buf); } //如果对端关闭了 if (netevents.lNetworkEvents & FD_CLOSE){ //关闭事件 WSACloseEvent(event_arr[i]); closesocket(sock_arr[i]); –event_count; //调整数组 adjust_sockarr(sock_arr, i, event_count); adjust_eventarr(event_arr, i, event_count); if (netevents.iErrorCode[FD_CLOSE_BIT] != 0){ _tprintf(TEXT(“close error , index:%d , error:%d .detail:\n”), i, netevents.iErrorCode[FD_CLOSE_BIT]); print_error(WSAGetLastError()); continue; } cli_len = sizeof(cli_addr); if (getpeername(sock_arr[i], (SOCKADDR*)&cli_addr, &cli_len) != SOCKET_ERROR){ printf(“peer ip:%s, port:%d\n”, inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); } printf(“sock_arr[%d] closed\n”, i); } } } WSACleanup(); return 0;} ...

December 28, 2018 · 3 min · jiezi

epoll select的限制 条件触发 边缘触发

结论: epoll 要优于 select , 编程模型基本一致; 请注意,不论是epoll 还是 select 都不是具有并发能力的服务器,仅仅是io复用题外话: 在io复用中把监听套接字设为非阻塞觉得理论麻烦的,可以直接往下拉,有代码例子;select 的缺陷:1.在sys/select.h 中 __FD_SETSIZE 为1024 , 意味默认情况最多监控1024个描述符,当然可以修改这个文件重新编译2.select 的实现 ,在fs/select.c 中:int do_select(int n, fd_set_bits fds, s64 timeout) { //略 for (i = 0; i < n; ++rinp, ++routp, ++rexp) { //略 } } 在实现中 , 每次将经过 n-1 次循环, 随着描述符越多,性能线性下降3.select 的3个 fd_set(read,write,except) 都是值-结果: 例如:fd_set rset,allset;while(1){ rset = allset; //每次需要重置 select(maxfd+1, rset, ….)}这种每次重置意味不断的从用户空间往内核空间中复制数据,操作系统一旦轮询完成后,再将数据置位,然后复制到用户空间,就是这种不断的复制造成了性能下降,还不得不这么干;epoll 解决了select缺陷;epoll 给每个需要监听的描述符都设置了一个或多个 event : struct epoll_event event; event.events = EPOLLIN; //读操作 ,如要监控多个可以 EPOLLIN|EPOLLOUT; event.data.fd = listensock; //赋值要监听的描述符, 就像 FD_SET(listensock,&rset);epoll的描述符限制可以查看 cat /proc/sys/fs/epoll/max_user_watches复制数据也仅仅在 epoll_ctl (…,EPOLL_CTL_ADD,…) 添加时(EPOLL_CTL_ADD),复制一次到epoll_create 所创建的红黑树中3. 最重要的是,epoll_wait 并不会像select 去轮询, 而是在内部给监听的描述符一个callback.一旦对应的事件发生(例如:EPOLLIN) 就将此事件添加到一个链表中.epoll_wait(此函数类似select) 的功能就是去链表中收集发生事件相应的 struct epoll_event;总的来说:epoll_create 在操作系统中创建一个用于存放struct epoll_event 的空间epoll_ctl 在空间内 添加,修改,删除 struct epoll_event (内含描述符);epoll_wait 收集已经发生事件的描述符;close 关闭(减少引用) epoll一个类似 select 的echo 服务器修改版本:echo.c#define EPOLL_SIZE 100 int listensock = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr, cli_addr; socklen_t socklen = sizeof(serv_addr); memset(&serv_addr,0,socklen); memset(&cli_addr,0,socklen); serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); serv_addr.sin_family = AF_INET; if(bind(listensock,(SA)&serv_addr,sizeof(serv_addr)) < 0){ perror(“bind”); return 0; } if(listen(listensock,BACKLOG) < 0){ perror(“listen”); return 0; } //向操作系统请求 创建一个 epoll , 参数看man int epfd = epoll_create(EPOLL_SIZE); if(epfd < 0){ perror(“epoll_create”); return 0; } //监听listensock ,类似 FD_SET(listensock, &rset) struct epoll_event event; event.events = EPOLLIN; //读取 . man中有详细解释 event.data.fd = listensock; if(epoll_ctl(epfd,EPOLL_CTL_ADD,listensock,&event) < 0) //把listensock 注册到epfd,用于监听event事件 { perror(“epoll ctl”); return 0; } //分配一块内存. 在 epoll_wait 返回后将存放发生事件的结构体 struct epoll_event * ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); if(ep_events == NULL){ perror(“malloc”); return 0; } puts(“server is running”); int n = 0; int client_fd = 0; char buf[BUFSIZ]; int len = 0,return_bytes = 0, tmp_fd = 0; while(1){ //收集已经发生事件的描述符 , 返回值与select一致 n = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1); if( n < 0){ perror(“epoll_wait”); break; } printf("%d events returned\n", n); for ( int i = 0; i < n ; ++i){ //如果有人连接 if( listensock == ep_events[i].data.fd){ socklen = sizeof(cli_addr); client_fd = accept(listensock,(SA*)&cli_addr,&socklen); //如果有人连接,则加入epoll event.events = EPOLLIN; //读取 event.data.fd = client_fd; if(epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event) < 0){ perror(“epoll add failed”); continue; } printf(“accepted,client_fd:%d,ip:%s,port:%d\n”, client_fd,inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port)); } else { tmp_fd =ep_events[i].data.fd; readagain: len = read(ep_events[i].data.fd,buf,BUFSIZ); //如果出错 if(len < 0) { if(errno == EINTR) goto readagain; else{ return_bytes = snprintf(buf,BUFSIZ-1, “clientfd:%d errorno:%d\n”,tmp_fd,errno); buf[return_bytes] = 0; epoll_ctl(epfd,EPOLL_CTL_DEL,tmp_fd,NULL); //从epoll中移除 close(tmp_fd); //close(socket) perror(buf); } } else if( 0 == len){ // 对端断开 return_bytes = snprintf(buf,BUFSIZ-1, “clientfd:%d closed\n”,tmp_fd); buf[return_bytes] = 0; epoll_ctl(epfd,EPOLL_CTL_DEL,tmp_fd,NULL); close(tmp_fd); puts(buf); } else{ //echo write(tmp_fd,buf,len); } } } }条件触发: epoll 的默认行为就是条件触发(select也是条件触发); 通过修改代码得结论 先添加一个BUFF_SIZE#define BUFF_SIZE 4把read(ep_events[i].data.fd,buf,BUFSIZ) 的BUFSIZ 修改成BUFF_SIZE然后 telnet 此服务器 : telnet 127.0.0.1 9988 , 随意写一些数据给服务器先看结果,这是我这里的输出:fuck@ubuntu:/sockettest$ ./epoll_serv server is running1 events returnedaccepted,client_fd:5,ip:127.0.0.1,port:544041 events returned1 events returned1 events returned1 events returned1 events returned1 events returned让read 最多只能读4个字节的唯一原因是 ,证明什么是条件触发通过结果得到结论: 只要此缓冲区内还有数据, epoll_wait 将不断的返回, 这个就是默认情况下epoll的条件触发;边缘触发:这个需要先做个实验才能理解,纯文字估计不太好理解;唯一需要修改的代码是在accept下面的一行:event.events = EPOLLIN | EPOLLET; // EPOLLET 就是边缘触发注意 read 那行代码,最多接受的字节最好在 14 之间 : read(…,…, 4);否则效果不明显接着telnet ,尝试一下每次写给服务器超过 5个字节我这里就不贴服务器输出了 , 可以看到这时 epoll_wait 无论怎么样只会返回一次了 ;先给结论: 只有当客户端写入(write,send,…) , epoll_wait 才会返回且只返回一次;注意与条件触发的不同: 条件触发情况下只要接受缓冲区有数据即返回, 边缘触发不会;由于这种只返回一次的特性 , EPOLLET 一般情况下都将采用非阻塞 O_NONBLOCK 的方式来读取; ...

December 26, 2018 · 2 min · jiezi

在io复用中把监听套接字设为非阻塞

往往在select 或 epoll 中把 listen_socket 设置为非阻塞 O_NONBLOCK原因是出在 accept 上, 比如有这么一个客户端: RST客户端当这个select或epoll 的服务器非常繁忙时, 有这么一个一连接就断开的客户端,此时 select 返回, 但还没执行到accept , 客户端就断开了, 然后执行到accept ,然后将一直阻塞一直阻塞到有客户连接为止, 而其他就绪的描述符将无法工作;如果将监听套接字设置为O_NONBLOCK ,就算有这么一个RST客户端, 执行到accepte 将返回一个错误EAGAIN或者EWOULDBLOCK,而不会一直阻塞在accpet调用上;设置O_NONBLOCK代码: int flag = fcntl(fd,F_GETFL,0); fcntl(fd,F_SETFL,flag|O_NONBLOCK);

December 26, 2018 · 1 min · jiezi

标准io 与 shutdown 半关闭

把文件描述符转标准io FILE 时 (例如: FILE readfp = fdopen(fd,“r”) )如果需要半关闭, 不能使用fclose ,fclose 将直接关闭(close)套接字还是需要shutdown 来帮忙 (例如 shutdown(fileno(readfp),SHUT_WR) ) ,先把 FILE* 转描述符,再调用shutdown;代码:serv.c int sock = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr,cli_addr; memset(&serv_addr,0,sizeof(serv_addr)); memset(&cli_addr,0,sizeof(cli_addr)); serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); serv_addr.sin_family = AF_INET; socklen_t cli_socklen = sizeof(cli_addr); bind(sock,(SA*)&serv_addr,sizeof(serv_addr)); listen(sock,10); int cli_sock = accept(sock,(SA*)&cli_addr,&cli_socklen); //把套接字转成标准io FILE* readfp = fdopen(cli_sock,“r”); //读指针 FILE* writefp = fdopen(cli_sock,“w”); //写指针 fputs(“fuck you 1”, writefp); fputs(“fuck you 2”, writefp); fputs(“fuck you 3”, writefp); fflush(writefp);// fclose(writefp); 这样将直接close, 下面的fgets 将收不到数据了 shutdown(fileno(writefp),SHUT_WR); // 还是使用shutdown 来半关闭 puts(“after shutdown”); system(“netstat -a | grep 9988”); getchar(); char buf[100] = {0}; if(fgets(buf,100,readfp) == NULL){ puts(" ******** peer closed ********* “); } printf(“read : %s\n”,buf); fclose(readfp); puts(“fclose read”); system(“netstat -a | grep 9988”);client.c if(argc !=3){ puts(“ip port”); return 0; } int sock = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr; memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family=AF_INET; serv_addr.sin_port = htons(atoi(argv[2])); serv_addr.sin_addr.s_addr = inet_addr(argv[1]); connect(sock,(SA*)&serv_addr,sizeof(serv_addr)); //把描述符转成标准io FILE * readfp = fdopen(sock,“r”); FILE* writefp = fdopen(sock,“w”); char buf[100]; while(1){ if(fgets(buf,sizeof(buf),readfp) == NULL){ puts(“server closed”); break; } printf(“buf:%s\n”,buf); fflush(stdout); } fputs(“byebye”,writefp); fflush(writefp); puts(“continue”); getchar(); fclose(writefp); fclose(readfp); ...

December 24, 2018 · 1 min · jiezi

多播

多播基于udp,让路由器复制数据包传递基本和udp 程序一样不同的地方:对于发送者重要的 ,1 发送数据不再直接发送到对端,而是发送到多播地址, 但端口还是对端的端口(否则对端套接字无法接受到数据),这样通过路由器复制再转发, 对端recvfrom 的ip 将是路由器2 多播ttl (默认1, 还是修改一下保险一些); send_addr.sin_addr.s_addr = inet_addr(ipaddr); //多播地址 send_addr.sin_port = htons(port); //对端端口 DWORD ttl = 64; //设置多播ttl,默认是1 有可能不太够用 setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,(char*)&ttl,sizeof(ttl));对于接受方 :1 由于是多播,由路由器复制并传递, 所以接受到的ip地址一般来说是路由器.2 需要bind一下port 用于接受数据,再来就是socket需要通过 setsockopt 加入多播组,否则无法接受多播组的信息以上2点代码示意: // bind ip,port , 否则无法接受数据 if (bind(sock, (SOCKADDR*)&local_addr, sizeof(local_addr)) == SOCKET_ERROR){ print_error(WSAGetLastError()); return 0; } //多播结构 IP_MREQ join_addr; join_addr.imr_interface.s_addr = INADDR_ANY; //加入多播的主机,一般来说就是本机 join_addr.imr_multiaddr.s_addr = inet_addr(ip); //需要加入的多播地址 //通过 IP_ADD_MEMBERSHIP 让socket 加入多播 setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&join_addr, sizeof(join_addr));全部代码:multi_sender.c WSADATA wsadata; WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); char ipaddr[16]; int port = 0; scanf(" %s %d", ipaddr,&port); SOCKADDR_IN send_addr; printf(“ip:%s\n”, ipaddr); memset(&send_addr, 0, sizeof(send_addr)); send_addr.sin_addr.s_addr = inet_addr(ipaddr); //发往多播地址 send_addr.sin_port = htons(port); //接受方的端口 send_addr.sin_family = AF_INET; DWORD ttl = 64; setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,(char*)&ttl,sizeof(ttl)); char buf[100] = “fuck you”; for (int i = 0; i < 3; ++i){ sendto(sock, buf, strlen(buf), 0, (SOCKADDR*)&send_addr, sizeof(send_addr)); Sleep(1000); } closesocket(sock); WSACleanup();mutli_recver.c char ip[16]; unsigned short port = 0; scanf(" %s %hd", ip, &port); WSADATA wsadata; WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); SOCKADDR_IN local_addr; memset(&local_addr, 0, sizeof(local_addr)); local_addr.sin_addr.s_addr = INADDR_ANY; local_addr.sin_family = AF_INET; local_addr.sin_port = htons(port); //接受的端口号 if (bind(sock, (SOCKADDR*)&local_addr, sizeof(local_addr)) == SOCKET_ERROR){ print_error(WSAGetLastError()); return 0; } //加入多播的结构 IP_MREQ join_addr; join_addr.imr_interface.s_addr = INADDR_ANY; //本机所有接口 join_addr.imr_multiaddr.s_addr = inet_addr(ip); //加入多播地址 //通过IP_ADD_MEMBERSHIP 加入多播组 setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&join_addr, sizeof(join_addr)); char buf[100]; SOCKADDR_IN client_addr; int cli_len = sizeof(client_addr); memset(&client_addr, 0, cli_len); int n = 0; while (1){ cli_len = sizeof(client_addr); n = recvfrom(sock, buf, sizeof(buf), 0, (SOCKADDR*)&client_addr, &cli_len); if (n == 0){ puts(“peer closed”); break; } else if (n == SOCKET_ERROR){ print_error(WSAGetLastError()); break; } else { buf[n] = 0; printf(“buf:%s, ip from : %s ,port:%d\n”, buf,inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } } WSACleanup(); ...

December 23, 2018 · 2 min · jiezi

获取外出接口

getsockname : 获取本机socket信息 ( 源ip/port)void get_out_int(char * ipaddr,unsigned short port){ //windows 初始化一下; //WSADATA wsadata; // WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_STREAM 一样 SOCKADDR_IN sock_addr,local_addr; memset(&local_addr, 0, sizeof(local_addr)); memset(&sock_addr, 0, sizeof(sock_addr)); sock_addr.sin_addr.s_addr = inet_addr(ipaddr); sock_addr.sin_port = htons(port); sock_addr.sin_family = AF_INET; if (connect(sock, (SOCKADDR*)&sock_addr, sizeof(sock_addr)) == SOCKET_ERROR) { print_error(WSAGetLastError()); closesocket(sock); return; } int len = sizeof(local_addr); getsockname(sock, (SOCKADDR*)&local_addr, &len); //获取套接字源信息 closesocket(sock); printf(“local addr:%s\n”, inet_ntoa(local_addr.sin_addr));}

December 23, 2018 · 1 min · jiezi

readv writev 简介 一次读写多个缓冲区

一个小例子说明函数使用:结构说明:struct iovec { void * iov_base //缓冲区地址 size_t iov_len //缓冲区输入/输出长度}#include “util.h”#include <sys/uio.h> int main(int argc , char **argv){ struct iovec v[2]; char buf1[] = “nihao”; char buf2[] = “fuck me”; v[0].iov_base = buf1; v[0].iov_len = 3; // 输入/输出 3个字节 v[1].iov_base = buf2; v[1].iov_len = 4; //输入/输出 4个字节 int n = writev(1,v,2); printf("\n write bytes:%d\n" , n); puts(“reading from stdin”); n = readv(0,v,2); printf(“read bytes:%d\n”,n); printf(“buf1:%s\n” ,buf1); printf(“buf2:%s\n”,buf2); return 0;}

December 21, 2018 · 1 min · jiezi

select 服务器 客户端 缩水版

tcpserver.cint main(int argc, charargv){ int listenfd = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr, client_addr; socklen_t client_len = sizeof(client_addr); memset(&serv_addr,0,sizeof(serv_addr)); memset(&client_addr,0,sizeof(client_addr)); serv_addr.sin_addr.s_addr = INADDR_ANY; //绑定所有ip serv_addr.sin_family=AF_INET; serv_addr.sin_port = htons(PORT); int opt = 1; socklen_t optlen = sizeof(opt); //设置复用ip if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,optlen) < 0){ perror(“setsockopt”); return 0; } //设置ip port if(bind(listenfd,(SA*)&serv_addr,sizeof(serv_addr)) < 0){ perror(“bind”); return 0; } //BACKLOG = 10 if(listen(listenfd,BACKLOG) < 0){ perror(“listen”); return 0; } //一些变量,下面会用到 int nready = 0,client[FD_SETSIZE] , maxfd = listenfd , connfd = 0,maxi = -1; // client 用于存储 客户描述符 for(int i = 0; i < FD_SETSIZE ; ++i) client[i] = -1; fd_set rset ,allset; FD_ZERO(&allset); //把监听套接字先置位 FD_SET(listenfd,&allset); int clientfd = -1 , n = 0 , i = 0; char buf[BUFSIZ]; while(1){ //select 每次将修改rset,需要重置 rset = allset; nready = select(maxfd+1,&rset,NULL,NULL,NULL); printf(“nread : %d \n” , nready); //可能被信号打断 if(nready < 0){ perror(“select”); continue; } //客户链接 进来 if(FD_ISSET(listenfd,&rset)){ client_len = sizeof(client_addr); connfd = accept(listenfd,(SA*)&client_addr,&client_len); printf(“a client : %d\n” , connfd); //找个位置放进去 for(i = 0; i < FD_SETSIZE; ++i){ if(client[i] < 0) { client[i] = connfd; break; } } //服务器已满 if(FD_SETSIZE == i){ close(connfd); puts(“server is full”); } else { //把客户fd 放入监听集合中 FD_SET(connfd,&allset); if(connfd > maxfd) maxfd = connfd; //client 索引 if( i > maxi) maxi = i; //如果数量为0 则不需要往下继续了 if(–nready == 0) continue; } } for(int i = 0 ; i <= maxi;++i){ if((clientfd = client[i]) <0 ) continue; //直到找到一个可读的fd if(FD_ISSET(clientfd,&rset)){ printf(“clientfd : %d is ready\n”, clientfd); //如果对断关闭了 if((n = read(clientfd,buf,BUFSIZ)) <= 0){ printf(“clientfd : %d closed\n”,clientfd); //清空当前fd 所存在的地方 close(clientfd); FD_CLR(clientfd,&allset); client[i] = -1; } else{ write(clientfd,buf,n); } if(–nready == 0) break; } } } return 0;}tcpclient.c void str_echo(int sockfd);int max(int a, int b){ return a > b ? a : b;}int main(int argc, charargv){ if(argc != 2){ puts(“ip addr”); return 0; } int sockfd = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_port = htons(PORT); sin.sin_family = AF_INET; //把字符串转成网络字节序 inet_pton(AF_INET,argv[1],&sin.sin_addr); connect(sockfd,(SA*)&sin,sizeof(sin)); str_echo(sockfd); return 0;} void str_echo(int sockfd){ int maxfd = sockfd; int eof = 0 , readn = 0 , fd_num = 0, n= 0; char buf[BUFSIZ]; fd_set rset; FD_ZERO(&rset); while(1) { //EOF ==》 CTRL+D if(0 == eof){ FD_SET(0,&rset); } //select 将修改rset ,每次重置 FD_SET(sockfd,&rset); maxfd = max(0,sockfd); fd_num = select(maxfd+1,&rset,NULL,NULL,NULL); printf(“fd_num : %d\n” , fd_num); //如果是套接字可读 if(FD_ISSET(sockfd,&rset)){ //服务器关闭 if((n=read(sockfd,buf,BUFSIZ)) <= 0){ if(eof == 1){ puts(“server closed”); break; } else { puts(“server ter”); break; } } buf[n] = 0; printf(“recv from serv:%s\n” , buf); } //如果是输入端可读 if(FD_ISSET(0,&rset)){ //如果ctrl + d if((n = read(0,buf,BUFSIZ)) <= 0){ printf(“client closing\n”); //先发送Fin , 等服务端close shutdown(sockfd,SHUT_WR); eof = 1; FD_CLR(0,&rset); continue; } write(sockfd,buf,n); } }} ...

December 21, 2018 · 2 min · jiezi

udp connect

在udp 上使用connect 的情况:需要获取icmp 的错误信息.2.如果需要向同一个ip地址多次 sendto , 用以减少不断的连接,断开.提高性能注意:udp 的connect 只记录(注册)对端的套接字结构(ip,port) , 并不会像tcp 进行3次握手.所以无法第一时间获取连接错误;这种称为有连接的udp套接字 也只能 发送/接受 connect中的指定的ip,port;下面一个例子说明了 sendto 只是向内核缓冲区复制数据 就返回了. 并不会产生icmp错误信息无connect 版本int main(int argc, charargv){ int sockfd = socket(AF_INET,SOCK_DGRAM,0); //udp struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_port = htons(PORT); //没有服务器 sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr(“127.0.0.1”); char send[100],recv[100]; int n = 0; while(1){ n = read(0,send,100); n =sendto(sockfd,send,n,0,(SA*)&sin,sizeof(sin)); //发送到缓冲区就返回, 没有icmp错误信息 printf(“sendto return : %d\n” , n); n = recvfrom(sockfd,recv,100,0,0,0); // 此时将一直阻塞在这里 等待接受数据 printf(“recvfrom return :%d\n”,n); recv[n] =0; printf(“buf : %s\n”, recv); } return 0;}通过调用connect 后的代码, 把sendto , recvfrom 换成了read , write (不是一定要换,只是后2个函数参数少)通过connect后的udp 套接字 可以收到icmp错误信息了, 在read返回后将产生错误, write仅仅是复制数据到缓冲区;#include “util.h”#include <netdb.h>extern int h_errno; int main(int argc, charargv){ int sockfd = socket(AF_INET,SOCK_DGRAM,0); struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_port = htons(PORT); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr(“127.0.0.1”); char send[100],recv[100]; int n = 0; puts(“begin connect”); int r = connect(sockfd,(SA*)&sin,sizeof(sin)); //udp的连接在这一步不会有问题 与 tcp不同, tcp会3次握手, udp没有 if(r < 0){ perror(“connect error”); return 0; } printf(“connect return :%d , errno:%d\n”, r, errno); while(1){ n = read(0,send,100); n = write(sockfd,send,n); //这里也不会有问题,仅仅发送到缓冲区. printf(“sendto return : %d\n” , n); n = read(sockfd,recv,100); //这里将返回错误Connection refused,这是没connect前所没有的 if(n < 0){ printf("***recvfrom return :%d ,error:%d\n",n,errno); perror("***read error"); } else { recv[n] =0; printf(“buf : %s\n”, recv); } } return 0;} ...

December 21, 2018 · 1 min · jiezi

用于消耗服务器资源的rst工具

以下代码可自行修改成 用于大量消耗服务器资源的工具.主要SO_LINGER 选项. 作用于close时, 直接发送 rst;例子:#include “util.h"int main(int argc, char**argv){ if(argc != 3){ puts(“ip port”); return 0; } int sockfd = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(atoi(argv[2])); sin.sin_addr.s_addr = inet_addr(argv[1]); connect(sockfd,(SA*)&sin,sizeof(sin)); struct linger li; li.l_onoff = 1; //on li.l_linger = 0; //rst on close; setsockopt(sockfd,SOL_SOCKET,SO_LINGER,&li,sizeof(li)); close(sockfd); return 0;}

December 21, 2018 · 1 min · jiezi

Linux安装Mysql5.6

由于安装的mysql8.0和其他服务器的数据库(版本5.1.30)由于版本差异过大,无法通信,因此需要安装一个中间版本5.6,但是它的安装过程和mysql8.0安装略有不同。解压文件// 解压文件生成两个xz格式的压缩文件$ tar -xzvf mysql-5.6.42-linux-glibc2.12-x86_64.tar.gz// 为了方便查找,改个名字mv mysql-5.6.42-linux-glibc2.12-x86_64 mysql5// 为了使用mysql快速初始化,链接到指定目录ln -s /home/work/lnmp/mysql5/ /usr/local/mysql环境配置我们需要专门的mysql进程启动用户和权限管理:// 创建mysql系统用户和用户组useradd -r mysql// 给予安装目录mysql权限chown mysql:mysql -R mysql5配置自己的mysql配置文件,因为我有多个Mysql库,我手动指定很多参数:[client]socket=/home/work/lnmp/mysql5/tmp/mysql.sockdefault-character-set=utf8[mysql]basedir=/home/work/lnmp/mysql5/datadir=/home/work/lnmp/mysql5/data/socket=/home/work/lnmp/mysql5/tmp/mysql.sockport=3306user=mysql# 指定日志时间为系统时间log_timestamps=SYSTEMlog-error=/home/work/lnmp/mysql5/log/mysql.err[mysqld]basedir=/home/work/lnmp/mysql5/datadir=/home/work/lnmp/mysql5/data/socket=/home/work/lnmp/mysql5/tmp/mysql.sockport=3306user=mysqllog_timestamps=SYSTEMcollation-server = utf8_unicode_cicharacter-set-server = utf8[mysqld_safe]log-error=/home/work/lnmp/mysql5/log/mysqld_safe.errpid-file=/home/work/lnmp/mysql5/tmp/mysqld.pidsocket=/home/work/lnmp/mysql5/tmp/mysql.sock[mysql.server]basedir=/home/work/lnmp/mysql5socket=/home/work/lnmp/mysql5/tmp/mysql.sock[mysqladmin] socket=/home/work/lnmp/mysql5/tmp/mysql.sock 这个里面我指定了错误日志的路径,在接下来的操作中,如果出现错误,除了查看终端显示的错误,还要记得去错误日志里查看详细的信息。因为我指定了一些文件,所以需要提前创建:mkdir logtouch log/mysql.errtouch log/mysqld_safe.errmkdir tmpmkdir datacd .. & chown mysql:mysql -R mysql5数据库初始化如果我们不初始化,直接使用bin/mysqld_safe启动会报错,因为我们需要初始化mysql环境,具体的操作可以参考官方文档:$ scripts/mysql_install_db –user=mysql…To start mysqld at boot time you have to copysupport-files/mysql.server to the right place for your systemPLEASE REMEMBER TO SET A PASSWORD FOR THE MySQL root USER !To do so, start the server, then issue the following commands: ./bin/mysqladmin -u root password ’new-password’ ./bin/mysqladmin -u root -h szwg-cdn-ai-predict00.szwg01.baidu.com password ’new-password’Alternatively you can run: ./bin/mysql_secure_installationwhich will also give you the option of removing the testdatabases and anonymous user created by default. This isstrongly recommended for production servers.See the manual for more instructions.You can start the MySQL daemon with: cd . ; ./bin/mysqld_safe &You can test the MySQL daemon with mysql-test-run.pl cd mysql-test ; perl mysql-test-run.plPlease report any problems at http://bugs.mysql.com/The latest information about MySQL is available on the web at http://www.mysql.comSupport MySQL by buying support/licenses at http://shop.mysql.comWARNING: Found existing config file ./my.cnf on the system.Because this file might be in use, it was not replaced,but was used in bootstrap (unless you used –defaults-file)and when you later start the server.The new default config file was created as ./my-new.cnf,please compare it with your file and take the changes you need.提示中提示我们已经创建了root的用户,需要修改临时密码,同时初始化成功。也告诉我们怎么启动一个数据库实例。启动数据库我们使用mysqld_safe 命令来启动:$ bin/mysqld_safe181217 14:55:08 mysqld_safe Logging to ‘/home/work/lnmp/mysql5/log/mysqld_safe.err’.181217 14:55:08 mysqld_safe Starting mysqld daemon with databases from /home/work/lnmp/mysql5/data链接全局命令此时,我们调用mysql只能用路径/home/work/lnmp/mysql8/bin/mysql或相对路径,需要链接为全局命令:$ ln -s /home/work/lnmp/mysql8/bin/mysql /usr/bin/$ ln -s /home/work/lnmp/mysql8/bin/mysql_safe /usr/bin/打开数据库数据库进程已经启动,我们可以在新终端正常使用mysql数据库,但是直接使用mysql命令报错:$ mysql -urootERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/tmp/mysql.sock’ (2)我查看了官方安装多个数据库的文档,尝试了很多方法,依然没有办法指定mysql命令的默认socket路径(/tmp/mysql.sock)。但是根据mysql.sock的作用的说明,我们指定mysql.sock路径即可:bin/mysql -S /home/work/lnmp/mysql8/tmp/mysql.sock -h localhost -uroot -pEnter password: 或者:ln -s /home/work/lnmp/mysql8/tmp/mysql.sock /tmp/然后我们再调用mysql命令就不会报错了。修改初始密码初始化的时候,命令行文本已经提示我们需要怎样更新root密码,并根据他的指示操作即可,要详细阅读输出的文本:$ bin/mysql_secure_installation NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MySQL SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY!In order to log into MySQL to secure it, we’ll need the currentpassword for the root user. If you’ve just installed MySQL, andyou haven’t set the root password yet, the password will be blank,so you should just press enter here.Enter current password for root (enter for none): OK, successfully used password, moving on…Setting the root password ensures that nobody can log into the MySQLroot user without the proper authorisation.Set root password? [Y/n] yNew password: Re-enter new password: Password updated successfully!Reloading privilege tables.. … Success!连接数据库,新密码已经更新。参考文章mysql8.0安装:https://segmentfault.com/a/11…mysql8.0初始化:https://dev.mysql.com/doc/ref… ...

December 17, 2018 · 2 min · jiezi

Linux安装mysql 8.0

经过一番努力下载mysql文件,我们可以开始Mysql8.0的安装了。 解压文件 // 解压文件生成两个xz格式的压缩文件$ tar -xvf mysql-8.0.13-linux-glibc2.12-x86_64.tarmysql-router-8.0.13-linux-glibc2.12-x86_64.tar.xzmysql-test-8.0.13-linux-glibc2.12-x86_64.tar.xz// 我们需要先删掉/移除原有文件,才可以继续解压,因为解压出来的.tar文件和.tar.xz文件重名mv mysql-8.0.13-linux-glibc2.12-x86_64.tar ../xz -d mysql-router-8.0.13-linux-glibc2.12-x86_64.tar.xztar -xvf mysql-8.0.13-linux-glibc2.12-x86_64.tar// 为了方便查找,改个名字mv mysql-8.0.13-linux-glibc2.12-x86_64.tar mysql8 环境配置 我们需要专门的mysql进程启动用户和权限管理: // 创建mysql系统用户和用户组useradd -r mysql// 给予安装目录mysql权限chown mysql:mysql -R mysql8 配置自己的mysql配置文件,因为我有多个Mysql库,我手动指定很多参数: [client]socket=/home/work/lnmp/mysql8/tmp/mysql.sockdefault-character-set=utf8 [mysql] basedir=/home/work/lnmp/mysql8/datadir=/home/work/lnmp/mysql8/data/socket=/home/work/lnmp/mysql8/tmp/mysql.sockport=3306user=mysql# 指定日志时间为系统时间log_timestamps=SYSTEMlog-error=/home/work/lnmp/mysql8/log/mysql.err# 指定字符集为utf8,因为mysql8.0中的默认字符集为utfmb4,会和其他程序引起兼容性问题default-character-set=utf8 ...

December 13, 2018 · 2 min · jiezi

PHP协程:并发 shell_exec

在PHP程序中经常需要用shell_exec执行一些命令,而普通的shell_exec是阻塞的,如果命令执行时间过长,那可能会导致进程完全卡住。在Swoole4协程环境下可以用Co::exec并发地执行很多命令。本文基于Swoole-4.2.9和PHP-7.2.9版本协程示例<?php$c = 10;while($c–) { go(function () { //这里使用 sleep 5 来模拟一个很长的命令 co::exec(“sleep 5”); });}协程结果htf@htf-ThinkPad-T470p:/workspace/debug$ time php t.phpreal 0m5.089suser 0m0.067ssys 0m0.038shtf@htf-ThinkPad-T470p:/workspace/debug$只用了 5秒,程序就跑完了。下面换成 PHP 的 shell_exec 来试试。阻塞代码<?php$c = 10;while($c–) { //这里使用 sleep 5 来模拟一个很长的命令 shell_exec(“sleep 5”);}阻塞结果htf@htf-ThinkPad-T470p:/workspace/debug$ time php s.php real 0m50.119suser 0m0.066ssys 0m0.058shtf@htf-ThinkPad-T470p:/workspace/debug$ 可以看到阻塞版本花费了50秒才完成。Swoole4提供的协程,是并发编程的利器。在工作中很多地方都可以使用协程,实现并发程序,大大提升程序性能。

November 29, 2018 · 1 min · jiezi

socket踩坑实录

socket简述socket(双工协议)网络中的两个程序,通过一个双向的连接来实现数据的交换,我们把连接的一端称为socketsocket特性自带连接保持可以实现双向通信socket分类基于TCP的socket基于UDP的socket基于RawIP的socket基于链路层的socket文章持续更新中~~~~

November 25, 2018 · 1 min · jiezi

PHP socket初探 --- 一些零碎细节的拾漏补缺

原文:https://t.ti-node.com/thread/…前面可以说是弄了一系列的php socket和多进程的一大坨内容,知识浅显、代码粗暴、风格简陋,总的说来,还是差了一些细节。今天,就一些漏掉的细节补充一下。一些有志青年可能最近手刃了Workerman源码,对于里面那一大坨stream_select()、stream_socket_server()表示疑惑,这个玩意和socket_create、socket_set_nonblock()有啥区别?其实,php官方手册里也提到过一嘴,socket系函数就是基于BSD Socket那一套玩意搞的,几乎就是将那些东西简单包装了一下直接抄过来用的,抄到甚至连名字都和C语言操控socket的函数一模一样,所以说socket系函数是一种比较低级(Low-Level,这里的低级是指软件工程中分层中层次的高低)socket操控方式,可以最大程度给你操作socket的自由以及细腻度。在php中,socket系本身是作为php扩展而体现的,这个你可以通过php -m来查看有没有socket,这件事情意味着有些php环境可能没有安装这个扩展,这个时候你就无法使用socket系的函数了。但stream则不同了,这货是内建于php中的,除了能处理socket网络IO外,还能操控普通文件的打开写入读取等,stream系将这些输入输出统一抽象成了流,通过流来对待一切。有人可能会问二者性能上差距,但是本人没有测试过,这个我就不敢轻易妄言了,但是从正常逻辑上推演的话,应该不会有什么太大差距之类的。一定要分清楚监听socket和连接socket,我们服务器监听的是监听socket,然后accept一个客户端连接后的叫做连接socket。关于“异步非阻塞”,这五个字到底体现在哪儿了。swoole我就不说了,我源码也才阅读了一小部分,我就说Workerman吧,它在github上称:“Workerman is an asynchronous event driven PHP framework with high performance for easily building fast, scalable network applications.”,看到其中有asynchronous(异步)的字样,打我脸的是我并没有看到有non-block(非阻塞)的字样,不过无妨,脸什么的不重要,重要的是我文章里那一坨又一坨的代码里哪里体现了非阻塞、哪里体现了异步。来吧,看代码吧。看代码前,你要理解异步和非阻塞的区别是什么,因为这二者在表现结果上看起来是有点儿相似的,如果你没搞明白,那么一定要通过这个来理解一下《PHP socket初探 — 关于IO的一些枯燥理论》。<?php// 创建一个监听socket,这个一个阻塞IO的socket$listen = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen, ‘0.0.0.0’, 9999 );socket_listen( $listen );while( true ){ // socket_accept也是阻塞的,虽然有while,但是由于accpet是阻塞的,所以这段代码不会进入无限死循环中 $connect = socket_accept( $listen ); if( $connect ){ echo “有新的客户端”.PHP_EOL; } else { echo “客户端连接失败”.PHP_EOL; }}将上面代码保存了运行一下,然后用telnet可以连接上去。但是,这段代码中有两处是阻塞的,最主要就是监听socket是阻塞的。那么,非阻塞的监听socket会是什么感受?<?php// 创建一个监听socket,将其设置为非阻塞$listen = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen, ‘0.0.0.0’, 9999 );socket_listen( $listen );// ⚠️⚠️⚠️⚠️⚠️⚠️ 这里设置非阻塞!socket_set_nonblock( $listen );while( true ){ $connect = socket_accept( $listen ); if( $connect ){ echo “有新的客户端”.PHP_EOL; } else { echo “客户端连接失败”.PHP_EOL; }}将代码保存了运行一下,告诉我:来来来,分析一波儿,为啥会出现这种现象。因为监听socket被设置成了非阻塞,我们知道非阻塞就是程序立马返回,然后再过段时间回来询问,用例子就是“等馒头过程中,看下微博,抬头问馒头好了吗?然后看下微信,抬头问馒头好了吗?然后看下v2ex,抬头问馒头好了吗?。。。 。。。”,这样你是不是就能理解了?因为并没有客户端连接进来,所以每当询问一次socket_accept后得到的反馈都是“没有连接”,所以就直接走到“客户端连接失败”的分支中去了,而且是不断的不停的。这个时候,你用htop或者top命令查看服务器CPU,不出意外应该是100%,这是非阻塞的极大缺点。紧接着是异步呢?异步体现在哪儿了?我们说异步,是你去阿梅那里买馒头,阿梅告诉你说“馒头还没好,你去干别的吧,好了我打电话通知你”,然后你就专心去打游戏去了,直到电话响了你去拿馒头。Workerman的异步更多是体现在对一个完整请求的处理流上,而不是正儿八经的异步的定义概念,如果你没听明白,那也可能正常,慢慢理解。最后,我补充一句:epoll是同步的,而不是异步。 ...

November 21, 2018 · 1 min · jiezi

PHP socket初探 --- 含着泪也要磕完libevent(三)

原文地址:https://t.ti-node.com/thread/…这段时间相比大家也看到了,本人离职了,一是在家偷懒实在懒得动手,二是好不容易想写点儿时间全部砸到数据结构和算法那里了。今儿回过头来,继续这里的文章。那句话是怎么说的:“自己选择的课题,含着泪也得磕完!”(图文无关,详情点击这里)。其实在上一篇libevent文章中(《PHP socket初探 — 硬着头皮继续libevent(二)》),如果你总结能力很好的话,可以观察出来我们尝试利用libevent做了至少两件事情:毫秒级别定时器信号监听工具大家都是码php的,也喜欢把自己说的洋气点儿:“ 我是写服务器的 ”。所以,今天的第一个案例就是拿libevent来构建一个简单粗暴的http服务器:<?php$host = ‘0.0.0.0’;$port = 9999;$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen_socket, $host, $port );socket_listen( $listen_socket );echo PHP_EOL.PHP_EOL.“Http Server ON : http://{$host}:{$port}".PHP_EOL;// 将服务器设置为非阻塞,此处概念可能略拐弯,建议各位查阅一下手册socket_set_nonblock( $listen_socket );// 创建事件基础体,还记得航空母舰吗?$event_base = new EventBase();// 创建一个事件,还记得歼15舰载机吗?我们将“监听socket”添加到事件监听中,触发条件是read,也就是说,一旦“监听socket”上有客户端来连接,就会触发这里,我们在回调函数里来处理接受到新请求后的反应$event = new Event( $event_base, $listen_socket, Event::READ | Event::PERSIST, function( $listen_socket ){ // 为什么写成这样比较执拗的方式?因为,“监听socket”已经被设置成了非阻塞,这种情况下,accept是立即返回的,所以,必须通过判定accept的结果是否为true来执行后面的代码。一些实现里,包括workerman在内,可能是使用@符号来压制错误,个人不太建议这>样做 if( ( $connect_socket = socket_accept( $listen_socket ) ) != false){ echo “有新的客户端:".intval( $connect_socket ).PHP_EOL; $msg = “HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nHi”; socket_write( $connect_socket, $msg, strlen( $msg ) ); socket_close( $connect_socket ); }}, $listen_socket );$event->add();$event_base->loop();将代码保存为test.php,然后php http.php运行起来。再开一个终端,使用curl的GET方式去请求服务器,效果如下:这是一个非常非常简单地不能再简单的http demo了,对于一个完整的http服务器而言,他还差比较完整的http协议的实现、多核CPU的利用等等。这些,我们会放到后面继续深入的文章中开始细化丰富。还记得我们使用select系统调用实现了一个粗暴的在线聊天室,select这种业余的都敢出来混个聊天室,专业的绝对不能怂。无数个专业???????????????送给libevent!啦啦啦啦,开始码:<?php$host = ‘0.0.0.0’;$port = 9999;$fd = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $fd, $host, $port );socket_listen( $fd );// 注意,将“监听socket”设置为非阻塞模式socket_set_nonblock( $fd );// 这里值得注意,我们声明两个数组用来保存 事件 和 连接socket$event_arr = []; $conn_arr = []; echo PHP_EOL.PHP_EOL.“欢迎来到ti-chat聊天室!发言注意遵守当地法律法规!".PHP_EOL;echo " tcp://{$host}:{$port}".PHP_EOL;$event_base = new EventBase();$event = new Event( $event_base, $fd, Event::READ | Event::PERSIST, function( $fd ){ // 使用全局的event_arr 和 conn_arr global $event_arr,$conn_arr,$event_base; // 非阻塞模式下,注意accpet的写法会稍微特殊一些。如果不想这么写,请往前面添加@符号,不过不建议这种写法 if( ( $conn = socket_accept( $fd ) ) != false ){ echo date(‘Y-m-d H:i:s’).’:欢迎’.intval( $conn ).‘来到聊天室’.PHP_EOL; // 将连接socket也设置为非阻塞模式 socket_set_nonblock( $conn ); // 此处值得注意,我们需要将连接socket保存到数组中去 $conn_arr[ intval( $conn ) ] = $conn; $event = new Event( $event_base, $conn, Event::READ | Event::PERSIST, function( $conn ) use( $event_arr ) { global $conn_arr; $buffer = socket_read( $conn, 65535 ); foreach( $conn_arr as $conn_key => $conn_item ){ if( $conn != $conn_item ){ $msg = intval( $conn ).‘说 : ‘.$buffer; socket_write( $conn_item, $msg, strlen( $msg ) ); } } }, $conn ); $event->add(); // 此处值得注意,我们需要将事件本身存储到全局数组中,如果不保存,连接会话会丢失,也就是说服务端和客户端将无法保持持久会话 $event_arr[ intval( $conn ) ] = $event; }}, $fd );$event->add();$event_base->loop();将代码保存为server.php,然后php server.php运行,再打开其他三个终端使用telnet连接上聊天室,运行效果如下所示:尝试放一张动态图试试,看看行不行,自己制作的gif都特别大,不知道带宽够不够。截止到这篇为止,死磕Libevent系列的大体核心三把斧就算是抡完了,弄完这些,你在遇到这些代码的时候,就应该不会像下面这个样子了: ...

November 20, 2018 · 2 min · jiezi

PHP socket初探 --- select系统调用

[原文地址:https://blog.ti-node.com/blog…]在<PHP socket初探 — 先从一个简单的socket服务器开始>中依次讲解了三个逐渐进步的服务器:只能服务于一个客户端的服务器利用fork可以服务于多个客户端的额服务器利用预fork派生进程服务于多个客户端的服务器最后一种服务器的进程模型基本上的大概原理其实跟我们常用的apache是非常相似的.其实这种模型最大的问题在于需要根据实际业务预估进程数量,依旧是需要大量进程来解决问题,可能会出现CPU浪费在进程间切换上,还有可能会出现惊群现象(简单理解就是100个进程在等带客户端连接,来了一个客户端但是所有进程都被唤醒了,但最终只有一个进程为这个客户端服务,其余99个白白折腾),那么,有没有一种解决方案可以使得少量进程服务于多个客户端呢?答案就是在<PHP socket初探 — 关于IO的一些枯燥理论>中提到的"IO多路复用".多路是指多个客户端连接socket,复用就是指复用少数几个进程,多路复用本身依然隶属于同步通信方式,只是表现出的结果看起来像异步,这点值得注意.目前多路复用有三种常用的方案,依次是:select,最早的解决方案poll,算是select的升级版epoll,目前的最终解决版,解决c10k问题的功臣今天说的是select,这个东西本身是个Linux系统调用.在Linux中一切皆为文件,socket也不例外,每当Linux打开一个文件系统都会返回一个对应该文件的标记叫做文件描述符.文件描述符是一个非负整数,当文件描述数达到最大的时候,会重新回到小数重新开始(题外话:按照传统,一般情况下标准输入是0,标准输出是1,标准错误是2).对文件的读写操作就是利用对文件描述符的读写操作.一个进程可以操作的文件描述符的数量是有限制的,不同系统有不同的数量,在linux中,可以通过调整ulimit来调整控制.先通过一个简单的例子说明下select的作用和功能.双11到了,你给少林足球队买了很多很多球鞋,分别有10个快递给你运送,然后你就不断地电话询问这10个快递员,你觉得有点儿累.阿梅很心疼你,于是阿梅就说:“这事儿你不用管了,你去专心练大力金刚腿吧,等任何一个快递到了,我告诉你”.当其中一个快递来了后,阿梅就喊你:"下来啦,有快递!",但是,这个阿梅比较缺心眼,她不告诉你是具体哪双鞋子的快递,只告诉你有快递到了.所以,你只能依次查询一遍所有快递单的状态才能确认是哪个签收了.上面这个例子通过结合术语演绎一遍就是,你就是服务器软件,阿梅就是select,10个快递就是10个客户端(也就是10个连接socket fd).阿梅负责替你管理着这10个连接socket fd,当其中任何一个fd有反应了也就是可以读数据或可以发送数据了,阿梅(select)就会告诉你有可以读写的fd了,但是阿梅(select)不会告诉你是哪个fd可读写,所以你必须轮循所有fd来看看是哪个fd,是可读还是可写.是时候机械记忆一波儿了:当你启动select后,需要将三组不同的socket fd加入到作为select的参数,传统意义上这种fd的集合就叫做fd_set,三组fd_set依次是可读集合,可写集合,异常集合.三组fd_set由系统内核来维护,每当select监控管理的三个fd_set中有可读或者可写或者异常出现的时候,就会通知调用方.调用方调用select后,调用方就会被select阻塞,等待可读可写等事件的发生.一旦有了可读可写或者异常发生,需要将三个fd_set从内核态全部copy到用户态中,然后调用方通过轮询的方式遍历所有fd,从中取出可读可写或者异常的fd并作出相应操作.如果某次调用方没有理会某个可操作的fd,那么下一次其余fd可操作时,也会再次将上次调用方未处理的fd继续返回给调用方,也就是说去遍历fd的时候,未理会的fd依然是可读可写等状态,一直到调用方理会.上面都是我个人的理解和汇总,有错误可以指出,希望不会误人子弟.下面通过php代码实例来操作一波儿select系统调用.在php中,你可以通过stream_select或者socket_select来操作select系统调用,下面演示socket_select进行代码演示:<?php// BEGIN 创建一个tcp socket服务器$host = ‘0.0.0.0’;$port = 9999;$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen_socket, $host, $port );socket_listen( $listen_socket );// END 创建服务器完毕 // 也将监听socket放入到read fd set中去,因为select也要监听listen_socket上发生事件$client = [ $listen_socket ];// 先暂时只引入读事件,避免有同学晕头$write = [];$exp = [];// 开始进入循环while( true ){ $read = $client; // 当select监听到了fd变化,注意第四个参数为null // 如果写成大于0的整数那么表示将在规定时间内超时 // 如果写成等于0的整数那么表示不断调用select,执行后立马返回,然后继续 // 如果写成null,那么表示select会阻塞一直到监听发生变化 if( socket_select( $read, $write, $exp, null ) > 0 ){ // 判断listen_socket有没有发生变化,如果有就是有客户端发生连接操作了 if( in_array( $listen_socket, $read ) ){ // 将客户端socket加入到client数组中 $client_socket = socket_accept( $listen_socket ); $client[] = $client_socket; // 然后将listen_socket从read中去除掉 $key = array_search( $listen_socket, $read ); unset( $read[ $key ] ); } // 查看去除listen_socket中是否还有client_socket if( count( $read ) > 0 ){ $msg = ‘hello world’; foreach( $read as $socket_item ){ // 从可读取的fd中读取出来数据内容,然后发送给其他客户端 $content = socket_read( $socket_item, 2048 ); // 循环client数组,将内容发送给其余所有客户端 foreach( $client as $client_socket ){ // 因为client数组中包含了 listen_socket 以及当前发送者自己socket,所以需要排除二者 if( $client_socket != $listen_socket && $client_socket != $socket_item ){ socket_write( $client_socket, $content, strlen( $content ) ); } } } } } // 当select没有监听到可操作fd的时候,直接continue进入下一次循环 else { continue; } }将文件保存为server.php,然后执行php server.php运行服务,同时再打开三个终端,执行telnet 127.0.0.1 9999,然后在任何一个telnet终端中输入"I am DOG!",再看其他两个telnet窗口,是不是感觉很屌?不完全截图图下:还没意识到问题吗?如果我们看到有三个telnet客户端连接服务器并且可以彼此之间发送消息,但是我们只用了一个进程就可以服务三个客户端,如果你愿意,可以开更多的telnet,但是服务器只需要一个进程就可以搞定,这就是IO多路复用diao的地方!最后,我们重点解析一些socket_select函数,我们看下这个函数的原型:int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )值得注意的是$read,$write,$except三个参数前面都有一个&,也就是说这三个参数是引用类型的,是可以被改写内容的.在上面代码案例中,服务器代码第一次执行的时候,我们要把需要监听的所有fd全部放到了read数组中,然而在当系统经历了select后,这个数组的内容就会发生改变,由原来的全部read fds变成了只包含可读的read fds,这也就是为什么声明了一个client数组,然后又声明了一个read数组,然后read = client.如果我们直接将client当作socket_select的参数,那么client数组内容就被修改.假如有5个用户保存在client数组中,只有1个可读,在经过socket_select后client中就只剩下那个可读的fd了,其余4个客户端将会丢失,此时客户端的表现就是连接莫名其妙发生丢失了.[原文地址:https://blog.ti-node.com/blog…] ...

September 2, 2018 · 1 min · jiezi

PHP socket初探 --- 关于IO的一些枯燥理论

[原文地址:https://blog.ti-node.com/blog…]要想更好了解socket编程,有一个不可绕过的环节就是IO.在Linux中,一切皆文件.实际上要文件干啥?不就是读写么?所以,这句话本质就是"IO才是王道".用php的fopen打开文件关闭文件读读写写,这叫本地文件IO.在socket编程中,本质就是网络IO.所以,在开始进一步的socket编程前,我们必须先从概念上认识好IO.如果到这里你还对IO没啥概念,那么我就通过几个词来给你一个大概的印象:同步,异步,阻塞,非阻塞,甚至是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.是不是晕了?截至到目前为止,你可以简单地认为只要搞明白这几个名词的含义以及区别,就算弄明白IO了,至少了可以继续往下看了.先机械记忆一波儿:IO分为两大种,同步和异步.同步IO:阻塞IO非阻塞IOIO多路复用(包括select,poll,epoll三种)信号驱动IO异步IO那么如何理解区别这几个概念呢?尤其是同步和阻塞,异步和非阻塞,看起来就是一样的.我先举个例子结合自己的理解来说明一下:你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,你自己看着点儿!".于是你就站在旁边只等馒头.此时的你,是阻塞的,是同步的.阻塞表现在你除了等馒头,别的什么都不做了.同步表现在等馒头的过程中,阿梅不提供通知服务,你不得不自己要等到"馒头出炉"的消息.你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,你自己看着点儿!".于是你就站在旁边发微信,然后问一句:"好了没?",然后发QQ,然后再问一句:"好了没?".此时的你,是非阻塞的,是同步的.非阻塞表现在你除了等馒头,自己还干干别的时不时会主动问问馒头好没好.同步表现在等馒头的过程中,阿梅不提供通知服务,你不得不自己要等到"馒头出炉"的消息.你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,蒸好了我打电话告诉你!".但你依然站在旁边只等馒头,此时的你,是阻塞的,是异步的.阻塞表现在你除了等馒头,别的什么都不做了.异步表现在等馒头的过程中,阿梅提供电话通知"馒头出炉"的消息,你只需要等阿梅的电话.你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,蒸好了我打电话告诉你!".于是你就走了,去买了双新球鞋,看了看武馆,总之,从此不再过问馒头的事情,一心只等阿梅电话.此时的你,是非阻塞的,是异步的.非阻塞表现在你除了等馒头,自己还干干别的时不时会主动问问馒头好没好.异步表现在等馒头的过程中,阿梅提供电话通知"馒头出炉"的消息,你只需要等阿梅的电话.如果你仔细品过上面案例中的每一个字,你就能慢慢体会到之所以异步和非阻塞,同步和阻塞容易混淆,仅仅是因为二者的表现形式稍微有点儿相似而已.阻塞和非阻塞关注的是:在等馒头的过程中,你在干啥.同步和异步关注的是:等馒头这件事,你是一直等到"馒头出炉"的结果,还是立即跑路等阿梅告诉你的"馒头出炉".重点的是你是如何得知"馒头出炉"的.所以现实世界中,最傻的人才会采用异步阻塞的IO方式去写程序.其余三种方式,更多的人都会选择同步阻塞或者异步非阻塞.同步非阻塞最大的问题在于,你需要不断在各个任务中忙碌着,导致你的大脑混乱,非常累.[原文地址:https://blog.ti-node.com/blog…]

September 2, 2018 · 1 min · jiezi

PHP socket初探 --- 先从一个简单的socket服务器开始

[原文地址:https://blog.ti-node.com/blog…]socket的中文名字叫做套接字,这种东西就是对TCP/IP的“封装”。现实中的网络实际上只有四层而已,从上至下分别是应用层、传输层、网络层、数据链路层。最常用的http协议则是属于应用层的协议,而socket,可以简单粗暴的理解为是传输层的一种东西。如果还是很难理解,那再粗暴地点儿tcp://218.221.11.23:9999,看到没?这就是一个tcp socket。socket赋予了我们操控传输层和网络层的能力,从而得到更强的性能和更高的效率,socket编程是解决高并发网络服务器的最常用解决和成熟的解决方案。任何一名服务器程序员都应当掌握socket编程相关技能。在php中,可以操控socket的函数一共有两套,一套是socket_系列的函数,另一套是stream_系列的函数。socket_是php直接将C语言中的socket抄了过来得到的实现,而stream_系则是php使用流的概念将其进行了一层封装。下面用socket_*系函数简单为这一系列文章开个篇。先来做个最简单socket服务器:<?php$host = ‘0.0.0.0’;$port = 9999;// 创建一个tcp socket$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );// 将socket bind到IP:port上socket_bind( $listen_socket, $host, $port );// 开始监听socketsocket_listen( $listen_socket );// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上while( true ){ // 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的 // 所以你不用担心while循环会将机器拖垮,不会的 $connection_socket = socket_accept( $listen_socket ); // 向客户端发送一个helloworld $msg = “helloworldrn”; socket_write( $connection_socket, $msg, strlen( $msg ) ); socket_close( $connection_socket );}socket_close( $listen_socket );将文件保存为server.php,然后执行php server.php运行起来。客户端我们使用telnet就可以了,打开另外一个终端执行telnet 127.0.0.1 9999按下回车即可。运行结果如下:简单解析一下上述代码来说明一下tcp socket服务器的流程:1.首先,根据协议族(或地址族)、套接字类型以及具体的的某个协议来创建一个socket。2.第二,将上一步创建好的socket绑定(bind)到一个ip:port上。3.第三,开启监听linten。4.第四,使服务器代码进入无限循环不退出,当没有客户端连接时,程序阻塞在accept上,有连接进来时才会往下执行,然后再次循环下去,为客户端提供持久服务。上面这个案例中,有两个很大的缺陷:1.一次只可以为一个客户端提供服务,如果正在为第一个客户端发送helloworld期间有第二个客户端来连接,那么第二个客户端就必须要等待片刻才行。2.很容易受到攻击,造成拒绝服务。分析了上述问题后,又联想到了前面说的多进程,那我们可以在accpet到一个请求后就fork一个子进程来处理这个客户端的请求,这样当accept了第二个客户端后再fork一个子进程来处理第二个客户端的请求,这样问题不就解决了吗?OK!撸一把代码演示一下:<?php$host = ‘0.0.0.0’;$port = 9999;// 创建一个tcp socket$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );// 将socket bind到IP:port上socket_bind( $listen_socket, $host, $port );// 开始监听socketsocket_listen( $listen_socket );// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上while( true ){ // 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的 // 所以你不用担心while循环会将机器拖垮,不会的 $connection_socket = socket_accept( $listen_socket ); // 当accept了新的客户端连接后,就fork出一个子进程专门处理 $pid = pcntl_fork(); // 在子进程中处理当前连接的请求业务 if( 0 == $pid ){ // 向客户端发送一个helloworld $msg = “helloworldrn”; socket_write( $connection_socket, $msg, strlen( $msg ) ); // 休眠5秒钟,可以用来观察时候可以同时为多个客户端提供服务 echo time().’ : a new client’.PHP_EOL; sleep( 5 ); socket_close( $connection_socket ); exit; }}socket_close( $listen_socket );将代码保存为server.php,然后执行php server.php,客户端依然使用telnet 127.0.0.1 9999,只不过这次我们开启两个终端来执行telnet。重点观察当第一个客户端连接上去后,第二个客户端时候也可以连接上去。运行结果如下:通过接受到客户端请求的时间戳可以看到现在服务器可以同时为N个客户端服务的。但是,接着想,如果先后有1万个客户端来请求呢?这个时候服务器会fork出1万个子进程来处理每个客户端连接,这是会死人的。fork本身就是一个很浪费系统资源的系统调用,1W次fork足以让系统崩溃,即便当下系统承受住了1W次fork,那么fork出来的这1W个子进程也够系统内存喝一壶了,最后是好不容易费劲fork出来的子进程在处理完毕当前客户端后又被关闭了,下次请求还要重新fork,这本身就是一种浪费,不符合社会主义主流价值观。如果是有人恶意攻击,那么系统fork的数量还会呈直线上涨一直到系统崩溃。所以,我们就再次提出增进型解决方案。我们可以预估一下业务量,然后在服务启动的时候就fork出固定数量的子进程,每个子进程处于无限循环中并阻塞在accept上,当有客户端连接挤进来就处理客户请求,当处理完成后仅仅关闭连接但本身并不销毁,而是继续等待下一个客户端的请求。这样,不仅避免了进程反复fork销毁巨大资源浪费,而且通过固定数量的子进程来保护系统不会因无限fork而崩溃。<?php$host = ‘0.0.0.0’;$port = 9999;// 创建一个tcp socket$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );// 将socket bind到IP:port上socket_bind( $listen_socket, $host, $port );// 开始监听socketsocket_listen( $listen_socket );// 给主进程换个名字cli_set_process_title( ‘phpserver master process’ );// 按照数量fork出固定个数子进程for( $i = 1; $i <= 10; $i++ ){ $pid = pcntl_fork(); if( 0 == $pid ){ cli_set_process_title( ‘phpserver worker process’ ); while( true ){ $conn_socket = socket_accept( $listen_socket ); $msg = “helloworldrn”; socket_write( $conn_socket, $msg, strlen( $msg ) ); socket_close( $conn_socket ); } }}// 主进程不可以退出,代码演示比较粗暴,为了不保证退出直接走while循环,休眠一秒钟// 实际上,主进程真正该做的应该是收集子进程pid,监控各个子进程的状态等等while( true ){ sleep( 1 );}socket_close( $connection_socket );将文件保存为server.php后php server.php执行,然后再用ps -ef | grep phpserver | grep -v grep来看下服务器进程状态:可以看到master进程存在,除此之外还有10个子进程处于等待服务状态,再同一个时刻可以同时为10个客户端提供服务。我们通过telnet 127.0.0.1 9999来尝试一下,运行结果如下图:好啦,php新的征程系列就先通过一个简单的入门开始啦!下篇将会讲述一些比较深刻的理论基础知识。[原文地址:https://blog.ti-node.com/blog…] ...

September 1, 2018 · 2 min · jiezi