TCP服务器网络编程设计流程详解
一、TCP 服务器设计流程
TCP连接的服务器模式的程序设计流程主要分为:套接字初始化( socket()函数),套接字与端口的绑定(bind()函数),设置服务器的侦听连接(listen()函数),接受客户端连接(accept()函数),接收和发送数据(read()函数、write()函数)并进行数据处理及处理完毕的套接字关闭(close()函数)。
1、socket() 函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数函数建立一个协议族为domain、协议类型为type、协议编号为 protocol的套接字文件 描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回-1。
函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。 通信协议族在文件sys/socket.h 中定义
函数socket()的参数 type用于设置套接字通信的类型。主要有
函数socket()的参数protocol一般设置为0。
函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过 获 errno 得。通常情况下造成函数 socket()失败的原因是输入的参数错误造成的,例如某个协议不存在等, 这时需要详细检查函数的输入参数。由于函数的调用不一定成功,在进行程序设计的时候,一定要检查返回值。
如果进行TCP编程,我们可以使用如下代码建立一个流式套接字:
//初始化socket套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == server_fd){//perror 根据error的值打印出错的原因perror("socket() error");return 0;}
2、bind() 函数
bind() 函数用于绑定一个地址端口,在建立套接字文件描述符成功后,需要对套接字进行地址和端 口的绑定,才能进行数据的接收和发送操作。
bind() 函数原型如下:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind() 函数的第一个参数sockfd是socket() 函数创建的文件描述符。bind() 函数的第二个参数addr指向一个结构为sockaddr参数的指针,sockaddr中包含了端口和IP地址的信息。在进行地址绑定的时候,需要先将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等结合在一起。
struct sockaddr 结构体的原型如下:
struct sockaddr{
sa_family_t sa_family://用地址簇中的哪个协议
char sa_data[14];
}
如果通过struct sockaddr 结构体设置IP地址、端口号等信息会非常复杂,在进行TCP编程时, 我们使用struct sockaddr_in结构体:
struct in_addr{ //IP 地址结构
_be32 s_addr;
};
struct sockaddr_in {
_kernel_sa_family_t sin_family; /* Address family
_be16 sin_port; /* Port number
struct in_addr sin_addr; /* Internet address
/* Pad to size of 'struct sockaddr'.*/
unsigned char _pad[_SOCK_SIZE_-sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};
bind() 函数的第三个参数addrlen是struct sockaddr 结构体的长度,所以一般设置为:sizeof (struct sockaddr 结构体);
我们可以通过如下代码绑定一个地址端口:
struct sockaddr_in in;
in.sin_family = AF_INET;
//设置端口号
in.sin_port = htons(8888);
//设置IP地址,使用自己设备的IP地址
in.sin_addr.s_addr = inet_addr("172.26.141.165");//将一个点分十进制的IP转换成一个长整型数int ret;
ret = bind(server_fd, (struct sockaddr*)&in, sizeof(struct sockaddr));
if (-1 == ret)
{//perror 根据error的值打印出错的原因perror("bind() error");return 0;
}
在上面的代码中我们对对口号进行赋值时使用了htons()函数,这个函数的功能是将主机字节序转换 成网络字节序,inet_addr也是将主机字节序转换成网络字节序。
(1)转换函数:
将主机字节序转换成网络字节序:
htons():表示对于short类型的变量,从主机字节序转换为网络字节序
htonl():表示对于long 类型的变量,从主机字节序转换为网络字节序
将网络字节序转换成主机字节序:
ntohs():表示对于short类型的变量,从网络字节序转换为主机字节序
ntohl():表示对于long类型的变量,从网络字节序转换为主机字节序
3、listen() 函数
listen()函数可以监听本地端口,listen函数原型如下:
#include <sys/types.h> /* See NOTES*/
#include <sys/socket.h>
int listen(int sockfd, int backlog);
当listen()函数成功运行时,返回值为0;当运行失败时,它的返回值为-1,并且设置errno值:
在了解listen函数的第二个参数之前,我们先了解下Linux内核协议栈是如何管理tcp连接的: linux内核会为一个tcp连接管理使用 两个队列 ,分别是:
半连接队列:用来保存SYN_SENT和SYN_RECV两个状态的连接。也就是三次握手还没有完成, 连接还没建立的连接。
全连接队列:用来保存保存ESTABLISHED状态的连接。三次握手已经完成的连接。
全连接队列存放三次握手成功的连接,如果当服务器不调用accept函数,没有将全连接队列的请求 拿出来,当该队列满的时候,客户端的连接就无法再过来,而是存放在半连接队列中,所以当全连接 满时,服务器会处于SYN_RECV状态,客户端处于SYN_SENT状态。
listen函数的第二个参数backlog指定的是全连接队列的长度(在不同的系统上有不同的实现,大部 分Linux中实际的长度是backlog + 1)。
Linux中全连接队列的最大长度是128,也可以通过修改Linux系统参数对长度进行修改。
我们可以通过如下代码监听:
listen(server_fd, 30);
4、accept() 函数
accept() 函数从“全连接队列”中取出一个节点,然后为该TCP连接创建一个新的文件描述符,该文件描述符可以用于与连接的客户端进行通信,因此“ accept函数的返回是在TCP三次握手之后“。如果“全连接队列”为空(没有新的客户端连接服务端),accept函数会阻塞。
accept()函数的原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
通过accept()函数可以得到成功连接客户端的IP地址、端口和协议族等信息,这个信息是通过参数 addr 获得的。当accept()函数返回的时候,会将客户端的信息存储在参数addr中。参数addrlen表示第2个参数( addr)所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。需要注意的是,在 accept中 addrlen参数是一个指针而不是结构,accept()函数将这个指针传给TCP/IP协议栈。 accept()函数返回连接服务端的客户端的文件描述符,如果失败返回值为-1,并且设置errno值:
使用代码示例:
int cli_fd;//表示连接到的服务端的客户端的文件描述符
struct sockaddr_in cli_addr;
socklen_t len;while(1)
{len = sizeof(struct sockaddr);cli_fd = accept(server_fd, (struct sockaddr*)&cli_addr, &len);printf("[ip: %s, port: %d] %s\n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port));
}
5、数据的接收
读取客户端的数据可以使用以下三个函数:
1)read() 数据接收函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read()函数从套接字sockfd中接收数据放到缓冲区buf中,buf的长度为len。 第1个参数sockfd是套接口文件描述符,它是由系统调用socket()返回的。第2个参数buf是一个指针, 指向接收网络数据的缓冲区。第3个参数len表示接收缓冲区的大小,以字节为单位。
使用示例代码:
char buf[1024];
//读取客户端发送的数据
memset(buf, 0, sizeof(buf));
int ret;
ret = read(cli_fd, buf, sizeof(buf)-1); //阻塞
//如果客户端主动断开了连接,read函数解除阻塞,并且返回0
if (0 == ret)
{close(cli_fd);return NULL;
}printf("cli: %d, %s\n", cli_fd, buf)
2)recv() 数据接收函数
recv()函数用于接收数据,函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
前三个跟read函数一致,recv()函数的参数flags用于设置接收数据的方式,可选择的值及含义在下表中列出(flags 的值可以是表中值的按位或生成的复合值)。
上表中的值的具体说明:
MSG_DONTWAIT:这个标志将单个IO操作设为非阻塞方式,而不需要在套接字 上打开非阻塞标志,执行IO操作,然后关闭非阻塞标志。
MSG_ERRQUEUE:该错误的传输依赖于所使用的协议。
MSG_OOB:这个标志可以接收带外数据,而不是接收一般数据。
MSG_PEEK:这个标志用于查看可读的数据,在recv()函数执行后,内核不会将这些数据丢弃。 MSG_TRUNC:在接收数据后,如果用户的缓冲区大小不足以完全复制缓冲区中的数据,则将 数据截断,仅靠复制用户缓冲区大小的数据。其他的数据会被丢弃。
MSG_WAITALL:这个标志告诉内核在没有读到请求的字节数之前不使读操作返回。
示例代码:
char buf[1024];
//读取客户端发送的数据
memset(buf, 0, sizeof(buf));
int ret;
ret = recv(cli_fd, buf, sizeof(buf)-1,MSG_DONTWAIT);
if (0 == ret)
{close(cli_fd);return NULL;
}printf("cli: %d, %s\n", cli_fd, buf)
3)recvfrom() 数据接收函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
第四个参数保存接收的客户端的相关信息,但是因为TCP是基于连接的传输协议,在调用accept函数 时就能够获取客户端的信息,所以recvfrom一般用于udp通信。
6、数据的发送:
1) write()函数用于发送数据,函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>ssize_t write(int sockfd, const void *buf, size_t len);
参数:
sockfd:TCP Socket 描述符。
buf:要发送的数据缓冲区。
count:要发送的字节数。
示例代码:
write(cli_fd, "hello", sizeof("hello"));
2) send()函数, 原型如下
#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd:TCP Socket 描述符。
buf:要发送的数据缓冲区。
len:要发送的字节数。
flags:可选的标志参数,用于控制发送行为,如 MSG_DONTWAIT、MSG_NOSIGNAL 等。
示例代码:
send(cli_fd, "hello", sizeof("hello"), MSG_DONTWAIT)
7、使用线程的示例TCP服务器编程代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>#define dbgout(arg...) \do{ \char b__[1024]; \sprintf(b__, arg);\fprintf(stdout, "[%s,%s,%d] %s", __FILE__, __func__, __LINE__, b__); \} while (0)//线程函数
void* func(void* arg)
{//线程分离pthread_detach(pthread_self());int cli_fd = (int)arg;char buf[1024];while (1){//读取客户端发送的数据memset(buf, 0, sizeof(buf));int ret;ret = read(cli_fd, buf, sizeof(buf) - 1);//默认阻塞//如果客户端主动断开了连接,read函数解除阻塞,并且返回0if (0 == ret){dbgout("%d cli closed",cli_fd);close(cli_fd);pthread_exit(0);}if (ret > 0){printf("cli_fd: %d, %s\n", cli_fd, buf);}ret = send(cli_fd, "ok", strlen("ok"), MSG_DONTWAIT);dbgout("send: %d\n", ret);}
}int main(int argc, char* argv[])
{int server_fd;//初始化socket套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == server_fd){//perror 根据error的值打印出错的原因perror("socket() error");return 0;}//绑定地址和端口struct sockaddr_in in;in.sin_family = AF_INET;//设置端口号in.sin_port = htons(8888);//设置地址in.sin_addr.s_addr = inet_addr("172.26.141.165");int ret;ret = bind(server_fd, (struct sockaddr*)&in, sizeof(struct sockaddr));if (-1 == ret){//perror 根据error的值打印出错的原因perror("bind() error");return 0;}//监听客户端的连接请求listen(server_fd, 30);int cli_fd;//表示连接到的服务端的客户端的文件描述符struct sockaddr_in cli_addr;socklen_t len;while (1){len = sizeof(struct sockaddr);cli_fd = accept(server_fd, (struct sockaddr*)&cli_addr, &len);unsigned short port;char ip[16] = { 0 };//将网络字节序的端口号转换成主机字节序port = ntohs(cli_addr.sin_port);//将网络字节序的32位IP地址转换成主机字节序的字符串strcpy(ip, inet_ntoa(cli_addr.sin_addr));printf("new client: %s %d\n", ip, port);//创建一个线程读取和处理连接到服务端的客户端的数据pthread_t t;pthread_create(&t, NULL, func, (void*)cli_fd);}return 0;
}