Socket编程(TCP)
2025-8-28
| 2025-8-29
Words 7050Read Time 18 min
type
status
date
slug
summary
tags
category
icon
password

Socket编程(TCP)

TCP网络编程

TCP和UDP在编程接口上是非常像的,TCP是面向连接的,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。接下来看一下:
我们已经知道,云服务器不允许绑定公网IP,所以这里我们直接使用INADDR_ANY绑定任意IP,端口号自己指定就行了。
初始化服务器:
进行网络通信首先要创建套接字。
socket第二个参数我们要写成SOCK_STREAM对应TCP协议面向字节流。
notion image
_sock < 0,说明套接字失败,那就没有必须进行了
notion image

listen

TCP服务器是面向连接的,所以当别人给我发数据时候不能直接发数据,必须先和我建立连接,这就意味着服务器必须时时刻刻知道他向我发起连接请求。所以必须设置socket为监听状态(为了获取新连接)
notion image
backlog:底层全连接队列的长度,这个参数在后面TCP协议的时候说 。
初始化服务器:1.创建socket,2.bind,3.设置socket为监听状态

TcpEchoServer代码实现version1

accept

TCP不能直接发数据 ,因为它是面向连接的。通信之前必须要先获取连接,因此首先要获取新连接。
notion image
从这个sockfd这里获取新连接。
accept函数后两个参数和recvfrom是一模一样的,这两个参数的含义也是一样的都是输入输出型函数,将来谁连的我,远端的客户端的ip和port是多少。所以需要这两个参数把客户端消息获取上来。
notion image
这些都不重要,最重要的是accept的返回值
notion image
成功时这个函数会从已接受的socket返回一个文件描述符!失败返回-1错误码被设置。
调用accept它的返回值也是文件描述符,而我们自己也建立一个文件描述符,那这两个文件描述符是什么意思?
下面举个例子理解:
今天我和一群朋友去杭州西湖旅游,玩累了准备找个地方吃饭,假设来了一个地方都是卖鱼的,王家鱼庄、李家鱼庄、张家鱼庄等等。每一家鱼庄门口都有一个拉客的人,张三是王家鱼庄的门口拉客的人。我们走着走着张三过来了,小哥小哥你们要不要吃饭啊,我们这里的鱼都是从西湖打上来的。我们感觉可以试试,于是张三就带我和我的朋友到王家鱼庄,到了门口张三就向大厅呼唤李四过来接客把我们带进去,李四过来招呼我们,给我们倒水介绍特殊菜。当我们在享受李四给我们带来的服务时,张三去那了?张三自己有自己的业务,他把我们招呼过来之后,转头就走了,又跑到路边找下一位客人了。当我和我的朋友在吃饭的期间,发现我们周边越来越热闹了,张三带着客人来然后在门口喊着让其他人招呼客人。李四给我们提供服务,王五给别的客人提供服务等等。张三一直干着这一件事情。
张三 : 拉客
李四、王五、赵六。。。:提供服务
张三就相当于我们传给accept的创建好的文件描述符
李四、王五、赵六。。。就相当于accept返回文件描述符
一个服务器可能被多个客户端来连接,李四、王五、赵六。。。每一个都是对应一个文件描述符对外提供服务的, 未来我们一旦建立好连接,服务器不能用创建好的文件描述符和客户端通信,就好比不能用张三给客人提供服务,而应该让accept的返回值文件描述符来给用户提供服务。

因为客户端和服务端通信需要【源ip ,目的ip】,【源端口,目的端口】,所以要bind。但是不需要显示bind,因为如果bind特定的端口,如果两个客户端都bind一样的端口,谁先启动谁成功bind,另一个就不能启动了。
  1. 我们的客户端要不要listen?
不需要,服务器 listen是因为有人要连接它,客户端是发起连接的。
  1. 那客户端要不要accept?
不需要,服务器accept也是因为有人要连接它,客户端是是发起连接的。
那客户端到底要什么呢?要发起连接!
发现连接我们写到启动客户端里
notion image
第一个参数通过哪个套接字发起连接
第二个参数你要向那个ip和port的服务端发起连接
第三个参数是这个结构体的长度
notion image
以前在udp是第一次sendto发现没有bind会调用bind绑定ip和port,而tcp这里是在connect会帮bind。

client.cc

notion image
为什么服务这里打印出来的文件描述符是4呢?
因为默认打开三个文件,0,1,2被占了,3被listensock占了,所以这里打印的是4
notion image
我们确实看到客户端发起的连接已经被服务端看到了并且连接了。
这里的问题为什么有两条连接呢?正常情况下不是一条连接吗?
一般而言,TCP确实在查找的时候建立连接成功,只会有一条连接!!!
但是今天我们做测试,客户端和服务端是在一台机器上的!!!
如果是两台主机,你是服务端你看到的就是上面的,你是客户端你看到的是下面的。即便只有一条连接也是全双工的!

关闭客户端:
notion image
notion image
这里可以看到客户端关了服务端立马读到了,客户端在连这个文件又变成4了,这说明客户端一关闭服务端就将刚刚的文件描述符关了,关了之后你在连接我给你的还是4,此时文件描述符就被重复使用了。

notion image
当我又开一个客户端去连接然后给服务端发送消息的时候,服务端并不会显示,只有当我把上一个客户端关闭后,然后才获取到新连接,这个文件描述符还是4,才会把我发的消息接收。
notion image
这是因为刚才所写的服务器,我们获取一个新连接之后,然后进程就去serverIO提供死循环服务了。人家不退,服务器就一直在HandlerIO给人家提供服务。

Server 多进程版本

获取新连接之后创建子进程,创建子进程,父进程的文件描述符会被子进程继承的,文件描述符所指的文件也都是一样的。所以说父进程曾经打开的listensock以及sock子进程都能看到。
创建子进程,让子进程对外提供服务。
notion image
这里要注意父进程的文件描述符被子进程继承下来了,但是父进程可是打开了多个文件描述符,所以子进程最少把自己的不需要的文件描述符关掉。
那父进程要干什么呢?
根据以前在进程哪里所学知识,父进程当然是要阻塞或者非阻塞等待回收子进程的资源了,否则子进程退出变成僵尸进程了,就造成内存资源泄漏了
notion image
但是这里要等待的时候,选择阻塞式等待还是非阻塞等待?
选择阻塞式等待,那不还是串行执行吗,属于脱裤子放屁多此一举创建子进程。选择非阻塞式等待,万一没有新连接来了一直在accept哪里等着连接,对子进程资源可能并没有回收干净造成内存资源泄漏。所以选择非阻塞式等待并不好!
如果非要让你阻塞式等待,要怎么做?
这里是这样做的,让子进程关闭listensock之后,子进程在创建一个子进程也就是孙子进程,让子进程退出!孙子进程提供服务。因为子进程退出了所以父进程等待会立马成功,然后继续向下执行代码。虽然父进程回收了子进程资源,但是并不影响孙子进程提供服务,等孙子进程提供完服务自己退出。你是孙子进程和父进程没有半毛钱关系(各管各儿子),孙子进程是一个孤儿进程,孤儿进程会被操作系统领养然后等它退了回收它。
测试:
notion image
看到现在可以多个用户同时连接了。但是多进程并不是一个好方法,因此子进程要拷贝一份父进程的东西。
上面还需要父进程自己回收子进程的资源太麻烦,我们知道子进程退出并不是默默退出的,它会发17号信号,不过系统默认对这个信号是忽略。
因此这里我们让子进程退出然后资源自动被回收。父进程自己忙自己的事情。
这里有个问题,子进程关闭了不用的listensock文件描述符,父进程要不要关闭sock文件描述符?
notion image
父进程没关sock文件描述符,客户端关闭后再连接,文件描述符是一直增长的状态。文件描述符终有用完的时候!
所以父进程一定要关闭提供服务的sock文件描述符,虽然父进程关闭sock但它不会造成文件关闭,因为有引用计数,等到引用计数到0的时候这个文件才会真正的关闭!

TcpEchoServer代码实现version2

测试:
这样就不会浪费文件描述符了
notion image

Server 多线程版

现在我们想用线程来解决为多人提供服务。
创建新线程,那主线程和新线程之间多文件描述符的态度是什么?
这个sock文件描述符能不能被新线程看到呢?
能!它们共享同一份资源!这里也不用敢像多进程那样让父子进程关闭对应的文件描述符那样做。它们共享同一份资源!
新线程创建好了,主线程也要回收新线程的资源。以前用的是pthread_join,但是在后面我们学过可以使用pthread_deatch进行线程分离,主线程就不用等了。

TcpEchoServer代码实现version3

notion image

Server 线程池版

思路是这样的,未来新连接来了,我们可以把新连接构成一个任务,然后放到线程池里,由线程池来进行统一处理。

TcpEchoServer代码实现version4

测试:
notion image

实现全部代码TcpEchoServer.hpp

实现远程命令版本

希望服务端能在自己的服务器上执行成功并且把结构返回给客户端。
我们这里写的是简单的,有些命令不能执行。
只需要再写一个完成这样任务的业务逻辑就好了,这就是解耦的好处!
这里我们需要用popen接口,这样就不需要我们像以前实现myshell那样,创建子进程然后在子进程中调用exec*系列的函数执行程序替换,那样麻烦了。
这个创建管道,创建子进程,子进程进行程序替换
notion image
popen相当于做了pipe+fork+exec*的工作
command:未来要执行的命令字符串
返回类型FILE * :通过管道以文件的方式把对应的执行结果写到文件中
type:对这个文件以什么方式打开 “w”、“r”等等
notion image
失败返回NULL,要不是fork创建子进程失败,要不pipe创建管道失败,要不内存申请失败

我这里实现使用的是白名单模式,防止其他人在使用客户端的时候使用rm命令删除我机器上的文件

Command.hpp

测试
notion image

CommandServer.hpp

服务端只需要修改一部分
  1. 回调函数,交给上层进行处理
  1. 这里就使用线程来执行任务

Client.cc

Server.cc

地址转换函数

我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
notion image
in_addr转字符串的函数:
notion image
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr

关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
notion image
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
notion image
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
所以在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题
网络转主机
notion image
网络转主机
notion image

InetAddr.hpp

windows客户端与linux服务端交汇

和udp一样:
测试:
notion image

断线重连

客胡端会面临服务器崩溃的情况,我们可以试着写⼀个客户端重连的代码,模拟并理解⼀些客户端行为,比如游戏客户端等
我这里采用状态机来实现
  • Linux
  • Socket编程(UDP)数据链路层
    Loading...