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); //将字符串点分十进制转为二进制地址 网络字节序 失败-1
char* inet_ntoa(struct in_addr __in); //将结构IP地址转为点分十进制字符串

//例如
in_addr_t dwIP=inet_addr("172.16.2.6"); //不合法则返回-1
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
/* Structure describing a generic socket address.  */
struct sockaddr {
__SOCKADDR_COMMON(sa_); /* Common data: address family and length. */ //协议族
char sa_data[14]; /* Address data. */ //目标IP地址和端口
};
/*
sa_family取值如下:
PF_UNIX:UNIX本地域协议族
PF_INET:IPv4协议族
PF_INET6:IPv6协议族
AF_UNIX:UNIX本地域地址族
AF_INET:IPv4地址族
AF_INET6:IPv6地址族
*/

该结构过小,如IPv6和UNIX等地址长度超长,且为了内存对齐,Linux采用新的通用套接字地址结构:

1
2
3
4
5
6
#define __ss_aligntype    unsigned long int
struct sockaddr_storage {
__SOCKADDR_COMMON(ss_); /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
__ss_aligntype __ss_align; /* Force desired alignment. */
};

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
//IPv4专用
/* Type to represent a port. */
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
struct in_addr{ //存储一个IP地址
in_addr_t s_addr;
};
/* Structure describing an Internet socket address. */
struct sockaddr_in { //大小与sockaddr结构长度一样 可相互强制转换
__SOCKADDR_COMMON(sin_); //协议族
in_port_t sin_port; /* Port number. */ //端口号 网络字节序
struct in_addr sin_addr; /* Internet address. */ //IP地址
unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)]; /* Pad to size of `struct sockaddr'. */ //为了与sockaddr结构保持相同大小的空字节
};

//IPv6专用
/* IPv6 address */
struct in6_addr {
union {
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
};
/* Ditto, for IPv6. */
struct sockaddr_in6 {
__SOCKADDR_COMMON(sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 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 //本地套接字地址结构体大小 单位字节
); //成功0 出错-1
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"); //这个ip地址必须是本机上有的
s_add.sin_port = htons(portnum);
if (-1 == bind(sfp, (struct sockaddr*)(&s_add), sizeof(struct sockaddr))) { //绑定
printf("bind fail:%d!\r\n", errno); //bind fail:99
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));//打印套接字地址里的ip和端口值
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, //协议族 IPv4为AF_INET IPv6为AF_INET6 其他在bits/socket.h
int type, //套接字类型 流套接字SOCK_STREAM 数据报套接字SOCK_DGRAM 原始套接字SOCK_RAW 其他在bits/socket_type.h
int protocol //通信协议 TCP为IPPROTO_TCP UDP为IPPROTO_UDP 为0则根据type参数自动选取
); //成功返回套接字描述符 否则-1 用errno获取错误码

bind给套接字绑定IP地址和端口号。

1
2
3
4
5
int bind(
int sockfd, //套接字描述符
const struct sockaddr* addr, //套接字地址
socklen_t addrlen //addr长度
); //成功0 否则-1 用errno

可用通配地址htonl(INADDR_ANY);替换inet_addr("xxx.xxx.xxx.xxx");等,意思是如果本机有多个网卡和多个IP,那么绑定所有本地的IP,都能连接并接收。

listen使流套接字处于监听状态,监听客户端发来的连接请求。

1
2
3
4
int listen(
int sockfd, //流套接字描述符
int backlog //连接请求队列所能容纳的客户连接请求最大数 超过该数后客户连接请求收到错误
); //成功0 否则-1

服务端用accept从处于监听状态的流套接字的客户连接请求队列中取出排在最前面的一个客户端请求,并创建一个新的套接字来与客户套接字创建连接通道。若连接成功,则返回新创建的套接字描述符,以后用该新套接字与客户套接字相互传输数据。

1
2
3
4
5
int accept(
int sockfd, //处于监听状态的流套接字描述符
struct sockaddr* _Nullable restrict addr, //返回新创建的套接字地址
socklen_t* _Nullable restrict addrlen //addr长度
); //失败-1

客户端用connect与服务器监听套接字建立连接。若为阻塞套接字,则返回值表示是否连接成功。也可改为非阻塞方式并设置连接超时时间,此时连接请求不会马上成功,返回-1,errno返回EINPROCESS表示操作正在进行中,之后可用select检查该连接是否建立成功。

1
2
3
4
5
int connect(
int sockfd, //套接字描述符
const struct sockaddr* addr, //对方套接字地址
socklen_t addrlen //addr大小
); //成功0 否则-1

send在已建立连接的套接字上发送数据。但在该函数内部,只是将缓冲区中数据发送到套接字内部的发送缓冲区中并返回,何时发送、发送多少数据都由内核底层协议决定。除send外其他套接字函数在执行开始前要等待套接字发送缓冲区中数据被协议传送完毕才继续。所以若本次发送数据到接收端出现网络错误,则后续套接字函数返回-1。send比较内核发送缓冲区与待发送数据长度,若剩余空间大于待发送长度,则将待发送数据拷贝到剩余空间中;若剩余空间小于待发送长度,则在阻塞模式下,待协议将内核发送缓冲区中数据发送并腾出空间,再将待发送数据拷贝到发送缓冲区;非阻塞模式下尽力拷贝并返回实际拷贝字节大小,若缓冲区无可用空间则返回-1并将errno置EGAIN表示不确定。

TCP有内核发送缓冲区,UDP没有,后者发送时直接将数据发送到网络上,不缓存。当接收端内核接收缓冲区收到数据报后就返回ACK,并不等recv到用户空间再返回。还有sendtosendmsg,这俩既可用于无连接也可用于基于连接的套接字,调用将被阻塞到数据被发送完,除非设为非阻塞模式。但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, //buf缓冲区大小
int flags //一般0
);
ssize_t sendto(
int sockfd,
const void buf[.size],
size_t size,
int flags,
const struct sockaddr* dest_addr, //目的网络地址
socklen_t addrlen //dest_addr字节数
); //成功返回实际发送字节数 否则-1 errno错误码
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, //buf缓冲区大小
int flags //一般0
); //对方用close关闭则返回接收字节数 否则-1 errno为EINTR/EWOULDBLOCK/EAGAIN时认为连接正常
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 //src_addr字节数
); //成功返回收到数据字节数 用close关闭返回0 其他-1 errno错误码

close关闭一个套接字。

1
2
3
4
#include <unistd.h>
int close(
int fd //要关闭的套接字描述符
); //成功0 否则-1

连续多次发送的内容可能会被拼接到一起发送,接收时也是接收整体,且当网络包长度有限制时,部分网络数据包会被截断到不同次接收。用于变长数据的发送与接收示例如下。服务端如下:

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); //使用端口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; //总共要发送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') //如果不是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"); //服务器的IP
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) {
//接收来自服务器的信息,每次最大只能接收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,//控制命令
... //命令参数
);/* glibc, BSD */ //成功0 否则-1 errno错误码
int ioctl(
int fd,
int op,
...
);/* musl, other UNIX */

/*
op可选命令:
FIONBIO 参数为0时将套接字设为阻塞模式 非0时设为非阻塞模式
FIONREAD 当套接字为流套接字时参数返回一次recv可读入的数据量(等于套接字中排队的数据总量)
当套接字为数据报套接字时参数返回套接字排队第一个数据报大小
FIOASYNC 设置或清除异步I/O
*/

//例如
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 //optval缓冲区大小
); //成功0 否则-1 errno获取错误码
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); //optVal为SOCK_STREAM
getsockopt(s,SOL_SOCKET,SO_ACCEPTCONN,(char*)&optVal,(socklen_t*)&optLen); //判断套接字是否处于监听状态
setsockopt(s,SOL_SOCKET,SO_KEEPALIVE,(char*)&optVal,optLen); //启用保活机制