C++后端开发入门-TCP服务器编程
格式转换
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <arpa/inet.h> typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }; in_addr_t inet_addr(const char* __cp); char* inet_ntoa(struct in_addr __in);
in_addr_t dwIP=inet_addr("172.16.2.6"); struct in_addr ia; ia.s_addr=dwIP; printf("real_ip=%s\n",inet_ntoa(ia));
|
套接字常用结构
⼀个套接字代表通信的⼀端,每端都有⼀个套接字地址,包含了IP地址和端⼝信息。套接字地址分为通用套接字地址和专用套接字地址。
通用套接字地址结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct sockaddr { __SOCKADDR_COMMON(sa_); char sa_data[14]; };
|
该结构过小,如IPv6和UNIX等地址长度超长,且为了内存对齐,Linux采用新的通用套接字地址结构:
1 2 3 4 5 6
| #define __ss_aligntype unsigned long int struct sockaddr_storage { __SOCKADDR_COMMON(ss_); char __ss_padding[_SS_PADSIZE]; __ss_aligntype __ss_align; };
|
Linux为不同协议族定义了不同套接字地址结构体,称为专用地址结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
typedef uint16_t in_port_t; typedef uint32_t in_addr_t; struct in_addr{ in_addr_t s_addr; };
struct sockaddr_in { __SOCKADDR_COMMON(sin_); in_port_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)]; };
struct in6_addr { union { uint8_t __u6_addr8[16]; uint16_t __u6_addr16[8]; uint32_t __u6_addr32[4]; } __in6_u; };
struct sockaddr_in6 { __SOCKADDR_COMMON(sin6_); in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; };
|
当一个套接字绑定了地址,可获取它的套接字地址。套接字通信需要在本地和远程两端建立套接字,所以获取套接字地址分为获取本地套接字地址和获取远程套接字地址。getsockname
可在以下两种情况下获取本地套接字地址,并可用getpeername
获取通信对端套接字地址。
- 本地套接字用
bind
获取地址。
- 本地套接字没有绑定地址,但用
connect
和远程建立了连接,此时内核分配一个地址给本地套接字。
1 2 3 4 5 6 7 8 9 10 11
| #include <sys/socket.h> int getsockname( int sockfd, struct sockaddr* localaddr, socklen_t* addrlen ); int getpeername( int sockfd, struct sockaddr* peeraddr, socklen_t* addrlen );
|
使用方法为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <cstdio> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <errno.h> int main() { int sfp; struct sockaddr_in s_add; unsigned short portnum = 10051; struct sockaddr_in serv = { 0 }; char on = 1; int serv_len = sizeof(serv); int err; sfp = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sfp) { printf("socket fail ! \r\n"); return -1; }; printf("socket ok !\n"); printf("ip=%s,port=%d\n", inet_ntoa(serv.sin_addr), ntohs(serv.sin_port)); setsockopt(sfp, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); memset(&s_add, 0, sizeof(struct sockaddr_in)); s_add.sin_family = AF_INET; s_add.sin_addr.s_addr = inet_addr("192.168.31.153"); s_add.sin_port = htons(portnum); if (-1 == bind(sfp, (struct sockaddr*)(&s_add), sizeof(struct sockaddr))) { printf("bind fail:%d!\r\n", errno); return -1; }; printf("bind ok !\n"); getsockname(sfp, (struct sockaddr*)&serv, (socklen_t*)&serv_len); printf("ip=%s,port=%d\n", inet_ntoa(serv.sin_addr), ntohs(serv.sin_port)); return 0; };
|
主机字节序与网络字节序互转:
1 2 3 4 5
| #include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
|
TCP
使用Socket需要头文件如下,其他头文件可在/usr/include/x86_64-linux-gnu/下找到。
1 2 3 4 5
| #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h>
|
用socket
创建一个阻塞套接字,并分配系统资源。协议族中,AF_开头用于BSD,PF_开头用于POSIX,目前两类宏定义的值相同,可以混用。
1 2 3 4 5 6
| #include <sys/socket.h> int socket( int domain, int type, int protocol );
|
用bind
给套接字绑定IP地址和端口号。
1 2 3 4 5
| int bind( int sockfd, const struct sockaddr* addr, socklen_t addrlen );
|
可用通配地址htonl(INADDR_ANY);
替换inet_addr("xxx.xxx.xxx.xxx");
等,意思是如果本机有多个网卡和多个IP,那么绑定所有本地的IP,都能连接并接收。
用listen
使流套接字处于监听状态,监听客户端发来的连接请求。
1 2 3 4
| int listen( int sockfd, int backlog );
|
服务端用accept
从处于监听状态的流套接字的客户连接请求队列中取出排在最前面的一个客户端请求,并创建一个新的套接字来与客户套接字创建连接通道。若连接成功,则返回新创建的套接字描述符,以后用该新套接字与客户套接字相互传输数据。
1 2 3 4 5
| int accept( int sockfd, struct sockaddr* _Nullable restrict addr, socklen_t* _Nullable restrict addrlen );
|
客户端用connect
与服务器监听套接字建立连接。若为阻塞套接字,则返回值表示是否连接成功。也可改为非阻塞方式并设置连接超时时间,此时连接请求不会马上成功,返回-1,errno返回EINPROCESS表示操作正在进行中,之后可用select
检查该连接是否建立成功。
1 2 3 4 5
| int connect( int sockfd, const struct sockaddr* addr, socklen_t addrlen );
|
用send
在已建立连接的套接字上发送数据。但在该函数内部,只是将缓冲区中数据发送到套接字内部的发送缓冲区中并返回,何时发送、发送多少数据都由内核底层协议决定。除send
外其他套接字函数在执行开始前要等待套接字发送缓冲区中数据被协议传送完毕才继续。所以若本次发送数据到接收端出现网络错误,则后续套接字函数返回-1。send
比较内核发送缓冲区与待发送数据长度,若剩余空间大于待发送长度,则将待发送数据拷贝到剩余空间中;若剩余空间小于待发送长度,则在阻塞模式下,待协议将内核发送缓冲区中数据发送并腾出空间,再将待发送数据拷贝到发送缓冲区;非阻塞模式下尽力拷贝并返回实际拷贝字节大小,若缓冲区无可用空间则返回-1并将errno置EGAIN表示不确定。
TCP有内核发送缓冲区,UDP没有,后者发送时直接将数据发送到网络上,不缓存。当接收端内核接收缓冲区收到数据报后就返回ACK,并不等recv
到用户空间再返回。还有sendto
和sendmsg
,这俩既可用于无连接也可用于基于连接的套接字,调用将被阻塞到数据被发送完,除非设为非阻塞模式。但send
只用于基于连接的套接字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ssize_t send( int sockfd, const void buf[.size], size_t size, int flags ); ssize_t sendto( int sockfd, const void buf[.size], size_t size, int flags, const struct sockaddr* dest_addr, socklen_t addrlen ); ssize_t sendmsg( int sockfd, const struct msghdr* msg, int flags );
|
用recv
从连接或无连接套接字上接收数据。阻塞模式时recv
等待直到接收缓冲区中有数据,非阻塞则直接返回-1,errno置EWOULDBLOCK。若消息太大,多余字节会被丢弃。recvfrom
用于面向连接和无连接的套接字,recv
只用于面向连接的套接字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ssize_t recv( int sockfd, void buf[.size], size_t size, int flags ); ssize_t recvfrom( int sockfd, void buf[restrict.size], size_t size, int flags, struct sockaddr* _Nullable restrict src_addr, socklen_t* _Nullable restrict addrlen );
|
用close
关闭一个套接字。
1 2 3 4
| #include <unistd.h> int close( int fd );
|
连续多次发送的内容可能会被拼接到一起发送,接收时也是接收整体,且当网络包长度有限制时,部分网络数据包会被截断到不同次接收。用于变长数据的发送与接收示例如下。服务端如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| #include <cstdio> #include <assert.h> #include <sys/time.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <malloc.h> #define BUF_LEN 300 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; struct MyData{ int nLen; char data[0]; }; int main() { int err, i, iRes; int sockSrv = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.s_addr = inet_addr("192.168.0.118"); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(8000); bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); listen(sockSrv, 5); SOCKADDR_IN addrClient; int cn = 0, len = sizeof(SOCKADDR); struct MyData* mydata; while (1) { printf("--------wait for client-----------\n"); int sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, (socklen_t*)&len); printf("--------client comes-----------\n"); cn = 5550; mydata = (MyData*)malloc(sizeof(MyData) + cn); mydata->nLen = htonl(cn); memset(mydata->data, 'a', cn); mydata->data[cn - 1] = 'b'; send(sockConn, (char*)mydata, sizeof(MyData) + cn, 0); free(mydata); char recvBuf[BUF_LEN]; do { iRes = recv(sockConn, recvBuf, BUF_LEN, 0); if (iRes > 0) { printf("\nRecv %d bytes:", iRes); for (i = 0; i < iRes; i++) printf("%c", recvBuf[i]); printf("\n"); } else if (iRes == 0) printf("\nThe client has closed the connection.\n"); else { printf("recv failed with error: %d\n", errno); close(sockConn); return 1; }; } while (iRes > 0); close(sockConn); puts("Continue monitoring?(y/n)"); char ch[2]; scanf("%s", ch, 2); if (ch[0] != 'y') break; }; close(sockSrv); return 0; };
|
客户端如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| #include <cstdio> #include <assert.h> #include <sys/time.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <malloc.h> #define BUF_LEN 250 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; int main() { int err; u_long argp; char szMsg[] = "Hello, server, I have received your message."; int sockClient = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.s_addr = inet_addr("192.168.0.118"); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(8000); err = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); if (-1 == err) { printf("Failed to connect to the server:%d\n", errno); return 0; }; char recvBuf[BUF_LEN]; int i, cn = 1, iRes; int leftlen; unsigned char* pdata; iRes = recv(sockClient, (char*)&leftlen, sizeof(int), 0); leftlen = ntohl(leftlen); printf("Need to receive %d bytes data.\n", leftlen); while (leftlen > BUF_LEN) { iRes = recv(sockClient, recvBuf, BUF_LEN, 0); if (iRes > 0) { printf("\nNo.%d:Recv %d bytes:", cn++, iRes); for (i = 0; i < iRes; i++) printf("%c", recvBuf[i]); printf("\n"); } else if (iRes == 0) puts("\nThe server has closed the send connection.\n"); else { printf("recv failed:%d\n", errno); close(sockClient); return -1; }; leftlen = leftlen - iRes; }; if (leftlen > 0) { iRes = recv(sockClient, recvBuf, leftlen, 0); if (iRes > 0) { printf("\nNo.%d:Recv %d bytes:", cn++, iRes); for (i = 0; i < iRes; i++) printf("%c", recvBuf[i]); printf("\n"); } else if (iRes == 0) puts("\nThe server has closed the send connection.\n"); else { printf("recv failed:%d\n", errno); close(sockClient); return -1; }; leftlen = leftlen - iRes; }; char sendBuf[100]; sprintf(sendBuf, "I'm the client. I've finished receiving the data."); send(sockClient, sendBuf, strlen(sendBuf) + 1, 0); memset(sendBuf, 0, sizeof(sendBuf)); puts("Sending data to the server is completed"); close(sockClient); getchar(); return 0; };
|
I/O控制
用ioctl
发送I/O控制命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <sys/ioctl.h> int ioctl( int fd, unsigned long op, ... ); int ioctl( int fd, int op, ... );
int iMode=0; ioctl(m_socket,FIONBIO,&iMode); int num=0; ioctl(0,FIONREAD,&num);
|
套接字选项
用getsockopt
获取套接字选项,用setsockopt
更改套接字选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <sys/types.h> #include <sys/socket.h> int getsockopt( int sockfd, int level, int optname, void optval[restrict * .optlen], socklen_t* restrict optlen ); int setsockopt( int sockfd, int level, int optname, const void optval[.optlen], socklen_t optlen );
|
选项级别指的使选项的适用范围或适用对象,常用的有:
选项级别 |
含义 |
SOL_SOCKET |
与协议无关,作用于套接字本身 |
SOL_LRLMP |
作用于IrDA协议 |
IPPROTO_IP |
作用于IPv4协议 |
IPPROTO_IPV6 |
作用于IPv6协议 |
IPPROTO_RM |
作用于可靠多播传输 |
IPPROTO_TCP |
适用于流式套接字 |
IPPROTO_UDP |
适用于数据报套接字 |
其中SOL_SOCKET级别常用选项有:
选项 |
获取/设置/两者皆可 |
含义 |
SO_ACCEPTCONN |
获取 |
套接字是否处于监听状态,为真则处于监听状态,只针对面向连接的协议 |
SO_BROADCAST |
两者皆可 |
套接字能否传送广播消息,为真则允许,只针对IPX、UDP/IPv4等支持广播的协议 |
SO_CONDITIONAL_ACCEPT |
两者皆可 |
到来的协议是否接受 |
SO_DEBUG |
两者皆可 |
是否允许输出调试信息,为真则允许 |
SO_DONTLINGER |
两者皆可 |
是否禁用SO_LINGER选项,为真则禁用 |
SO_DONTROUTE |
两者皆可 |
是否禁用路由选择,为真则禁用 |
SO_ERROR |
获取 |
获取套接字错误码 |
SO_KEEPALIVE |
两者皆可 |
套接字连接是否能保活,为真则能保活 |
SO_LINGER |
两者皆可 |
设置或获取当前拖延值。拖延值指关闭套接字时若有未发送的数据,则等待的时间 |
SO_MAX_MSG_SIZE |
获取 |
数据报套接字消息的最大尺寸,对流式套接字无意义 |
SO_OOBINLINE |
两者皆可 |
是否可在常规数据流中接收带外数据,为真则可以 |
SO_PROTOCOL_INFO |
获取 |
获取绑定到套接字的协议信息 |
SO_RCVBUF |
两者皆可 |
获取或设置内核接收缓冲区大小 |
SO_REUSEADDR |
两者皆可 |
是否允许套接字绑定一个已使用的地址 |
SO_SNDBUF |
两者皆可 |
获取或设置内核发送缓冲区大小 |
SO_TYPE |
获取 |
获取套接字类型 |
常用IPPROTO_IP选项如下:
选项 |
获取/设置/两者皆可 |
含义 |
IP_OPTIONS |
两者皆可 |
获取或设置IP头部内选项 |
IP_HDRINCL |
两者皆可 |
是否将IP头部与数据一起提交给套接字函数 |
IP_TTL |
两者皆可 |
TTL相关 |
常见错误码:
错误码 |
含义 |
EBADF |
不是有效的文件描述符 |
EFAULT |
用户缓冲区太小或非法 |
EINVAL |
选项级别非法 |
ENOPROTOOPT |
选项未知或协议族不支持 |
ENOTSOCK |
描述符不是套接字描述符 |
例子:
1 2 3 4 5 6
| int s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP),optVal,optLen=sizeof(optVal); getsockopt(s,SOL_SOCKET,SO_RCVBUF,(char*)&optVal,(socklen_t*)&optLen); getsockopt(s,SOL_SOCKET,SO_SNDBUF,(char*)&optVal,(socklen_t*)&optLen); getsockopt(s,SOL_SOCKET,SO_TYPE,(char*)&optVal,(socklen_t*)&optLen); getsockopt(s,SOL_SOCKET,SO_ACCEPTCONN,(char*)&optVal,(socklen_t*)&optLen); setsockopt(s,SOL_SOCKET,SO_KEEPALIVE,(char*)&optVal,optLen);
|