欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > 传输层协议 — TCP协议与套接字

传输层协议 — TCP协议与套接字

2025/3/28 12:58:04 来源:https://blog.csdn.net/m0_74234485/article/details/146405180  浏览:    关键词:传输层协议 — TCP协议与套接字

TCP套接字

UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。

因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态。

listen

int listen(int sockfd, int backlog);

参数说明

• sockfd:需要设置为监听状态的套接字对应的文件描述符。
• backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。

返回值说明

• 监听成功返回0,监听失败返回-1,同时错误码会被设置 

说明一下:

  • 初始化TCP服务器时创建的套接字并不是普通的套接字,而应该叫做监听套接字。为了表明寓意,我们将后面代码中套接字的名字由sock改为listen socket。
  • 在初始化TCP服务器时,只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成。

服务端获取连接——accept

TCP服务器初始化后就可以开始运行了,但TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端的连接请求。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明

  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。

返回值说明

  •  获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。

accept函数返回的套接字是什么?

调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。

监听套接字与accept函数返回的套接字的作用:

监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。

服务端获取连接

服务端在获取连接时需要注意:

accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接。
如果要将获取到的连接对应客户端的IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将端口号由网络序列转换成主机序列。
inet_ntoa函数在底层实际做了两个工作,一是将网络序列转换成主机序列,二是将主机序列的整数IP转换成字符串风格的点分十进制的IP

服务端接收连接测试

现在我们可以做一下简单的测试,看看当前服务器能否成功接收请求连接。在运行服务端时需要传入一个端口号作为服务端的端口号,然后我们用该端口号构造一个服务端对象,对服务端进行初始化后启动服务端即可。

服务端处理请求
现在TCP服务器已经能够获取连接请求了,下面当然就是要对获取到的连接进行处理。但此时为客户端提供服务的不是监听套接字,因为监听套接字获取到一个连接后会继续获取下一个请求连接,为对应客户端提供服务的套接字实际是accept函数返回的套接字,下面就将其称为“服务套接字”。

为了让通信双方都能看到对应的现象,我们这里就实现一个简单的回声TCP服务器,服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出,并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出,此时就能确保服务端和客户端能够正常通信了。

read函数

TCP服务器读取数据的函数叫做read,该函数的函数原型如下:

ssize_t read(int fd, void *buf, size_t count);


参数说明:

fd:特定的文件描述符,表示从该文件描述符中读取数据。
buf:数据的存储位置,表示将读取到的数据存储到该位置。
count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

如果返回值大于0,则表示本次实际读取到的字节个数。
如果返回值等于0,则表示对端已经把连接关闭了
如果返回值小于0,则表示读取时遇到了错误。
read返回值为0表示对端连接关闭

这实际和本地进程间通信中的管道通信是类似的,当使用管道进行通信时,可能会出现如下情况:

  • 写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
  • 读端进程不读,写端进程一直写,此时当管道被写满后写端进程就会被挂起,因为此时空间没有就绪。
  • 写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
  • 读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。

这里的写端就对应客户端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。

write函数

TCP服务器写入数据的函数叫做write,该函数的函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
buf:需要写入的数据。
count:需要写入数据的字节个数。

返回值说明:

写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。

服务端处理请求

需要注意的是,服务端读取数据是服务套接字中读取的,而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字,既可以读取数据也可以写入数据,这就是TCP全双工的通信的体现。

在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。

客户端不需要进行绑定和监听

服务端要进行绑定是因为服务端的IP地址和端口号必须要众所周知,不能随意改变。而客户端虽然也需要IP地址和端口号,但是客户端并不需要我们进行绑定操作,客户端连接服务端时系统会自动指定一个端口号给客户端。
服务端需要进行监听是因为服务端需要通过监听来获取新连接,但是不会有人主动连接客户端,因此客户端是不需要进行监听操作的。
此外,客户端必须要知道它要连接的服务端的IP地址和端口号,因此客户端除了要有自己的套接字之外,还需要知道服务端的IP地址和端口号,这样客户端才能够通过套接字向指定服务器进行通信。

单执行流的服务器

通过实验现象可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器,这个服务器一次只能为一个客户端提供服务。

当服务端调用accept函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是单执行流的,只能服务完当前客户端后才能继续服务下一个客户端。

客户端为什么会显示连接成功?

当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上来罢了。

实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。

多进程版的TCP网络程序
我们可以将当前的单执行流服务器改为多进程版的服务器。

当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务,而是当前执行流调用fork函数创建子进程,然后让子进程为父进程获取到的连接提供服务。

由于父子进程是两个不同的执行流,当父进程调用fork创建出子进程后,父进程就可以继续从监听套接字当中获取新连接,而不用关心获取上来的连接是否服务完毕。

子进程继承父进程的文件描述符表

需要注意的是,文件描述符表是隶属于一个进程的,子进程创建后会继承父进程的文件描述符表。比如父进程打开了一个文件,该文件对应的文件描述符是3,此时父进程创建的子进程的3号文件描述符也会指向这个打开的文件,而如果子进程再创建一个子进程,那么子进程创建的子进程的3号文件描述符也同样会指向这个打开的文件。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词