当前位置: 首页 > ai >正文

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;
}

http://www.xdnf.cn/news/17579.html

相关文章:

  • Linux Ansible的安装与基本使用
  • Linux:企业级WEB应用服务器TOMCAT
  • 技术干货|Kafka 如何实现零停机迁移
  • Stereolabs ZED相机 选型指南:双目 / 单目、短距 / 长距,如何为机器人视觉系统匹配最优方案?
  • selenium常见的与浏览器版本不兼容闪退问题
  • 计算机网络2-2:物理层下面的传输媒体
  • 【Node.js从 0 到 1:入门实战与项目驱动】2.2 验证安装(`node -v`、`npm -v`命令使用)
  • 计算机视觉(4)-相机基础知识恶补
  • Flink Redis维表:Broadcast Join与Lookup Join对比及SQL示例
  • 从零部署Nacos:替代Eureka的服务注册与服务发现基础教程
  • 使用Excel制作甘特图
  • 无人机三维路径规划
  • Python科学计算与可视化领域工具TVTK、Mayavi、Mlab、Traits(附视频教程)
  • 【PyTorch学习笔记 - 02】 Datasets DataLoaders
  • 白板功能文档
  • 物联网、大数据与云计算持续发展,楼宇自控系统应用日益广泛
  • 在达梦数据库中使用group by 命令报错问题
  • uniapp常用组件
  • OpenBMC中C++单例模式架构与实现全解析
  • PySpark性能优化与多语言选型讨论
  • 13-docker的轻量级私有仓库之docker-registry
  • golang 基础案例_02
  • 使用Pytest进行接口自动化测试(三)
  • Docker-09.Docker基础-Dockerfile语法
  • Selenium元素定位不到原因以及怎么办?
  • K8S学习----应用部署架构:传统、虚拟化与容器的演进与对比
  • 计算机网络(一)——TCP
  • monorepo架构设计方案
  • LCR 120. 寻找文件副本
  • 【bug】diff-gaussian-rasterization Windows下编译 bug 解决