目录
[2.Echo Server 单进程](#2.Echo Server 单进程)
[3.Echo Server 多进程](#3.Echo Server 多进程)
[4.Echo Server 多线程](#4.Echo Server 多线程)
[5.Echo Server 线程池](#5.Echo Server 线程池)
[6.Echo Server 多线程 远程执行](#6.Echo Server 多线程 远程执行)
[7.Dict Server 线程池](#7.Dict Server 线程池)
1.接口预备
1.1socket()
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
应用程序可以像读写文件一样用read/write在网络上收发数据;
如果socket()调用出错则返回-1,且错误码被设置;
对于IPv4,family参数指定为AF_INET;
对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议
protocol参数暂时忽略,指定为o即可(不需要指定协议了,前面的参数已经固定了是tcp)。
1.2bind()
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号;
bind()成功返回0,失败返回-1,且错误码被设置。
bind()的作用是将参数sockfd和addr(里面有网络参数,比如ip地址和端口号,协议家族等)绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
前面讲过,struct sockaddr*是一个通用指针类型,addr参数实际上可以接受多种协议的
sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
对sockaddr_in结构对象初始化如下:
cppbzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT);servaddr和SERV_PORT是自己定义的一个变量。
bzero将整个结构体清零(因为有ipv4的结构有填充字节,保险起见,先清0);
设置地址类型为AF_INET(表示IPV4);
网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址;
端口号为SERV_PORT,我们定义为9999(看你自己,在1024~60000多里选,如果是云服务器,记得安全规则把端口开放了);
1.3listen()
listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5),具体这个参数的理解,后面的文章我会再写。
listen()成功返回0,失败返回-1,错误码被设置;
1.4accept()
三次握手完成后,服务器调用accept()接受连接;
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
addr是一个传出参数,accept()成功返回时传出客户端的地址和端口号;
如果给addr参数传NULL,表示不关心客户端的地址;
addrlen参数是一个传入传出参数(value-resultargument),传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
返回失败时,返回-1,错误码被设置。
返回成功时,也会返回一个文件描述符。
cppwhile(1) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddr_len); n = read(connfd, buf, MAXLINE); ..... close(connfd); }注意,我们最开始创建的套接字或者说文件描述符,他只负责把接收到的客户端带进来,具体的针对每个客户端,服务端都会新创建一个套接字或者说文件描述符来负责这个客户端的通信任务。
1.5connect()
客户端需要调用connect()连接服务器;
connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的(服务端)地址;
conenct 就是客户端socket尝试与服务器端那个已经绑定了的socket建立一条通信通道。
核心功能
connect()将文件描述符 sockfd所代表的 socket 连接到 addr指定的地址。
内核将本地socket与本地公网ip+port的网络信息进行绑定。
两种不同类型 socket 的行为差异
1. 面向连接的 socket(SOCK_STREAM/SOCK_SEQPACKET)
典型代表: TCP 协议
行为: 尝试与指定地址的 socket 建立连接
特点:通常只能成功 connect()一次
建立的是可靠的双向通信通道
需要对方 socket 正在监听(listen())
2. 无连接的 socket(SOCK_DGRAM)
典型代表: UDP 协议
行为: 设置默认的目标地址后续发送的数据包都会发往这个地址
只接收来自这个地址的数据包
特点:可以多次调用 connect()来改变关联地址
可以"解除关联":通过连接到 AF_UNSPEC地址族
connect()成功返回0,出错返回-1,错误码被设置;
1.6send()&&recv()
用于网络层面的流式发送。跟write接口很像,唯一区别在于多了个flags,这个flags暂时不谈,后面再说,这里只需要设成0即可。
我们写tcp的时候,既可以read write也可以用send,但更推荐send
同理,对标read,以及udp的recvfrom。
1.7popen()
popen是在底层建立管道和子进程,子进程执行command指令(linux下的指令),结果通过FILE*返回,type的值是r、w等,参考open调用。成功返回用于读写管道的FILE*指针,如果子进程创建失败或者管道创建失败,或者你的栈没有足够的空间了,返回空指针。
用完了记得pclose关闭。成功返回指令的退出码,失败就返回错误码,又或者一些错误被检测到了返回-1。
简化了我们自己fork,pipe,exec,dup的流程。
2.Echo Server 单进程
echo服务,客户端输入的内容,交给服务端后原封不动的再传回客户端,在客户端上打印。
单进程版本同时只能负责一个客户端的通信,其他客户端就算连上了,也要等上个客户端退出连接了才能通信(在这之前发消息只能是延迟返回,比如当轮到他之后,所有反馈一起出现)
3.Echo Server 多进程
多进程的开销比较大,不推荐。
4.Echo Server 多线程
相比多进程,多线程系统开销会更小。
5.Echo Server 线程池
减少请求响应的时间,提早创建线程,一次性调用系统调用,减少开销。
6.Echo Server 多线程 远程执行
7.Dict Server 线程池
基本就是把之前udp的翻译模块拿过来,然后套进来就行了。
8.注意事项
我的代码中,tcp接受数据采用的是recv。因为tcp面向字节流,所以recv也能接受,但是问题在于,实际上我们的数据很可能需要完整的,一次就接受完,比如我第6个远程指令一样。看起来好像没影响啊,不都是字节流传过来吗,但是对于庞大的字节流来说,tcp不会一次性传过来,很可能是间隔性的传递,这样的话,比如我第6个任务的远程指令就直接被破坏了(当然,因为我的指令比较短,所以这里不会出现问题,但这个问题对于庞大的字节流来说是存在的)。
所以,对于tcp,我们必须在应用层面保证收到的数据是一个完整的请求!
另外,udp是面向数据报的,一旦数据传输,就是完整一次性传过来,所以udp不需要考虑这个问题。
针对这个问题,就需要序列化和反序列化。
9.Windows版本客户端
关于Windows环境的套接字事项,我前面udp已经讲了很多了,我这边补充下tcp的。
listen() :将套接字设置为监听模式,等待客户端的连接请求。
accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端进行通信。
10.支持断线重连的客户端
枚举类型
c:
枚举成员直接暴露在外层作用域中,容易造成命名冲突。
允许隐式转换为整型(如 int),可能引发意外行为。
底层类型由编译器决定(通常是 int),不可控。
无法前向声明,因为编译器需知道其底层类型大小。
被视为整型别名,不同枚举类型可互相赋值(通过整型中转)。
C++:
枚举成员属于枚举类的作用域,需通过 枚举类名::成员访问。
禁止隐式转换,必须显式转换(如 static_cast)。
允许显式指定底层类型(如 char、short),增强可移植性。
支持前向声明(尤其是指定底层类型时)。
是独立的类型系统,禁止不同枚举类间的直接比较或赋值。`````````````````````




















