TCP服务器C语言实现

参考链接:https://www.geeksforgeeks.org/tcp-server-client-implementation-in-c/

建立HTTP服务器的流程都是一样的,可以参考TCP Server的建立过程,因为Http是基于Tcp协议的,底层的原理都是相通的,参考了man手册大概介绍一下。

创建socket

创建socket,此时会返回一个描述符,头文件为#include <sys/socket.h>,函数原型为int socket(int domain, int type, int protocol);,domain参数指明通信将在哪个通信族发生,一般选PF_INET即Iternet版本4协议,socket参数指明通信的类型,可选择SOCK_STREAM/SOCK_DGRAM/SOCK_RAW,SOCK_STREAM类型提供基于序列的,可靠的,双向连接的字节流,可以支持带外数据传输机制,指的是TCP。 SOCK_DGRAM套接字支持数据报(无连接,不可靠的最大固定长度(通常很小)的消息),指的是UDP。 SOCK_RAW套接字提供对内部网络协议和接口的访问,仅超级用户可用。protocal协议指明与套接字使用的特定的协议,一般填0即可。
SOCK_STREAM类型的套接字是全双工字节流,类似于管道。 流套接字必须处于连接状态,然后才能在其上发送或接收任何数据。 通过connect或connectx调用创建与另一个套接字的连接。 连接后,可以使用read和write调用或send和recv调用的某些变型函数传输数据。 当会话已完成时,可以执行close。 带外数据也可以按照send中的说明进行发送,也可以按照recv中的说明进行接收。
如果发生错误,则返回-1,否则返回值是引用套接字的描述符。
所以可以这么写:

1
2
3
4
5
6
7
8
// socket create and verification 
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("socket creation failed...\n");
exit(0);
}
else
printf("Socket successfully created..\n");

绑定服务器的地址和端口

绑定服务器的地址和端口,原型函数为:int bind(int socket, const struct sockaddr *address, socklen_t address_len);,bind()为未命名的套接字分配一个名称。 使用socket创建套接字时,该套接字存在于名称空间(地址族)中,但未分配名称,bind()请求将该地址分配给套接字。在UNIX域中绑定名称会在文件系统中创建一个套接字,当不再需要该套接字时,调用方必须将其删除(使用unlink)。成功完成后,将返回值0。 否则,将返回值-1,并将全局整数变量errno设置为该指示错误。
参考实现:

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in servaddr;
// assign IP, PORT
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
// Binding newly created socket to given IP and verification
if ((bind(sockfd, (SA*)&servaddr, sizeof(servaddr))) != 0) {
printf("socket bind failed...\n");
exit(0);
}
else
printf("Socket successfully binded..\n");

查看sys/socket.h,netinet/in.h,apr_network_io.h文件,其中sockaddr、socketaddr_in结构体的定义为:
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
/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
/*
* Socket address, internet style.
*/
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
/**
* We need to make sure we always have an in_addr type, so APR will just
* define it ourselves, if the platform doesn't provide it.
*/
struct in_addr {
apr_uint32_t s_addr; /**< storage to hold the IP# */
};

开启监听

开启监听,监听套接字上的连接,函数原型为int listen(int socket, int backlog);,listen()调用仅适用于SOCK_STREAM类型的套接字。
backlog参数定义未决连接队列的最大长度,表示该服务端支持的最大连接数。如果连接请求在队列已满的情况下到达,则客户端可能会收到带有ECONNREFUSED指示的错误。 如果基础协议支持重传,则可以忽略该请求,以便重试可以成功。
如果成功,listen()函数将返回值0;否则,返回0。 否则,将返回值-1,并且将全局变量errno设置为该指示错误。
参考实现:

1
2
3
4
5
6
7
// Now server is ready to listen and verification 
if ((listen(sockfd, 5)) != 0) {
printf("Listen failed...\n");
exit(0);
}
else
printf("Server listening..\n");

接受连接

https://hackernoon.com/tcp-three-way-handshake-4161eb8aba32

接受连接,此过程发生在三次握手之后,函数原型为:int accept(int socket, struct sockaddr restrict address, socklen_t restrict address_len);,参数socket是使用socket创建的套接字,并使用bind绑定到地址,并在listen之后监听连接。 accept()提取未决连接队列上(队列的大小为backlog)的第一个连接请求,创建具有与套接字相同属性的新套接字,并为套接字分配新的文件描述符。 如果队列上没有待处理的连接,并且套接字没有标记为非阻塞,则accept()会阻塞调用者,直到出现连接为止。 如果套接字被标记为非阻塞并且队列上没有挂起的连接,则accept()将返回错误。
参数address是一个结果参数,该参数填充有通信层所知的连接实体的地址,即发起连接的客户端的地址。地址参数的确切格式由发生通信的域确定。address_len是一个值结果参数; 它最初应包含地址指向的空间量;返回时,它将包含返回地址的实际长度(以字节为单位)。该调用与基于连接的套接字类型一起使用,当前与SOCK_STREAM一起使用。
调用将在错误时返回-1,并将全局变量errno设置为指示错误。如果成功,则返回非负整数,该整数是已接受套接字的描述符。
如下所示,有两个队列:SYN队列(或不完整的连接队列)和接受队列(或完整的连接队列)。在三向握手中,服务器从客户端接收到SYN数据包后,将连接信息放置在SYN队列中,并将SYN-ACK数据包发送回客户端。然后,服务器从客户端接收ACK数据包。 如果接受队列未满,则应该从SYN队列中删除信息并将其放入接受队列。
三次握手与socket
此时,如果接受队列已满且tcp_abort_on_overflow为0,则服务器将在一定时间后再次向客户端发送SYN-ACK数据包(换句话说,它重复握手的第二步)。 如果客户端经历了短暂的超时,则很容易遇到客户端异常。
有几种方式可以检测接受队列是否满了,netstat -s | egrep “listen|LISTEN”,隔一段时间运行一下,看下溢出的次数是否有增加。或者直接通过ss -lnt查看当前队列值有多少了。就像ss命令一样,也可以通过netstat命令显示Send-Q和Recv-Q。 但是,如果连接未处于“侦听”状态,则Recv-Q表示接收到的数据仍在高速缓存中,并且尚未被进程读取。 该值表示该进程尚未读取的字节。 发送是远程主机尚未确认的发送队列中的字节数。重要的是要注意,netstat -tn显示的Recv-Q数据与接受队列或SYN队列无关。 这里必须强调这一点,以免将其与ss -lnt所示的Recv-Q数据混淆。
接受队列的大小取决于min(backlog,somaxconn)。 创建套接字时将传递backlog,因此somaxconn是操作系统级别的系统参数。SYN队列的大小取决于max(64,/proc/sys/net/ipv4/tcp_max_syn_backlog),不同版本的OS可能不同。
参考实现:

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in cli; 
int len = sizeof(cli);
// Accept the data packet from client and verification
connfd = accept(sockfd, (SA*)&cli, &len);
if (connfd < 0) {
printf("server acccept failed...\n");
exit(0);
}
else
printf("server acccept the client...\n");

数据收发

数据收发,建立连接后就可以进行通信了,注意需要基于accept后的新的套接字进行操作。参考实现如下:

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
// Function designed for chat between client and server. 
void func(int sockfd)
{
char buff[MAX];
int n;
// infinite loop for chat
for (;;) {
bzero(buff, MAX);

// read the message from client and copy it in buffer
read(sockfd, buff, sizeof(buff));
// print buffer which contains the client contents
printf("From client: %s\t To client : ", buff);
bzero(buff, MAX);
n = 0;
// copy server message in the buffer
while ((buff[n++] = getchar()) != '\n')
;

// and send that buffer to client
write(sockfd, buff, sizeof(buff));

// if msg contains "Exit" then server exit and chat ended.
if (strncmp("exit", buff, 4) == 0) {
printf("Server Exit...\n");
break;
}
}
}

关闭连接

关闭连接,头文件为#include <unistd.h>,函数原型为int close(int fildes);,在套接字的最后一次关闭时,关联的命名信息和排队的数据将被丢弃;当进程退出时,将释放所有关联的文件描述符,但是由于每个进程的活动描述符受到限制,因此在处理大量文件描述符时,close()函数调用非常有用,也就是说虽然进程退出时会自动释放该文件描述符,但是最后还是调用close()保险一点。成功完成后,将返回值0。 否则,将返回值-1并将全局整数变量errno设置为指示错误。

1
2
// After chatting close the socket 
close(sockfd);

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!