实现基于TCP的服务器端
#include
/*
* @params
* sock: 服务器套接字文件描述符。
* backlog: 等待连接的请求的队列长度
*/
int listen(int sock, int backlog); // 0: 成功。 -1: 失败。
#include
/*
* @params
* sock: 服务器套接字文件描述符。这里的套接字只是为了接收连接的,因为连接本身也是一种数据,需要用套接字来接收。
* addr: 客户端地址。传入时是空值,接收到连接后填入该客户端的地址信息。
* addrlen: 第二个参数addr结构的长度。在调用后填入。
*/
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); // 成功返回新创建的套接字文件描述符,失败返回-1。
实现基于TCP的客户端
#include
/*
* @params
* sock: 客户端套接字文件描述符。这里的套接字只是为了接收连接的,因为连接本身也是一种数据,需要用套接字来接收。
* addr: 目标服务器地址。
* addrlen: 第二个参数addr结构的长度。
*/
int connect(int sock, struct sockaddr *addr, socklen_t addrlen); // 0: 成功。 -1: 失败。
客户端的IP地址就是主机的IP地址,端口在调用connect()时自动由内核随机分配。
发生以下两种情况之一就会返回:
- 服务器端将请求记录到等待队列。
- 发生断网等异常情况而中断请求。
实现迭代服务器/客户端
# ./eserver 9190
Connected client 1
Connected client 2
Connected client 3
Connected client 4
Connected client 5
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): orange
Message from server: orange
Input message (Q to quit): bottle
Message from server: bottle
Input message (Q to quit): q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): when
Message from server: when
Input message (Q to quit): where
Message from server: where
Input message (Q to quit): q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): FIRST
Message from server: FIRST
Input message (Q to quit): Q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): sunny
Message from server: sunny
Input message (Q to quit): smile
Message from server: smile
Input message (Q to quit): q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): last one
Message from server: last one
Input message (Q to quit): q
这只是一个简单的例子而已。实际上客户端服务器的处理不是很谨慎,如果他们不在同一台机器上,发生了网络延迟等状况,可能会粘包或分包。
习题
- 请说明TCP/IP的4层协议栈,并说明TCP和UDP套接字经过的层级结构差异。
从低到高依次是数据链路层、网络层、传输层和应用层。TCP和UDP的差异在传输层。- 请说出TCP/IP协议栈中链路层和IP层的作用,并给出两者关系。
链路层提供物理连接,IP层基于物理链路选择合适的路径。- 为何需要把TCP/IP协议栈分成4层(或7层)?结合开放式系统回答。
为了通过标准化操作设计开放式系统。- 客户端调用connect函数向服务器端发送连接请求。服务器端调用哪个函数后,客户端可以调用connect函数?
必须在服务器端调用listen函数后。- 什么时候创建连接请求等待队列?它有何作用?与accept有什么关系?
listen函数创建连接请求等待队列。使得同时只能处理一个客户端连接的服务器暂存其他客户端的连接,以待后续处理。accept从队列中取第一个进行服务。- 客户端中为何不需要调用bind函数分配地址?如果不调用bind函数,那何时、如何向套接字分配IP地址和端口号?
因为在这里客户端是主动发起连接的一端,它不需要在某个固定的地址和端口去监听连接。在调用connect的时候内核会自动随机分配地址和端口,服务器可以根据收到的消息解析出客户端的地址和端口。- 把第1章的hello_server.c改成迭代服务器段,并利用客户端测试更改是否准确。
同本章例子。
我的问题
- 服务器端是阻塞在listen()还是accept()?
阻塞在accept()。listen()的作用只是使主动连接套接字变为被连接套接字,使得一个进程可以接受其它进程的请求。因此客户端的connect()可以发生在listen()的前面或后面。如果在前面则进入等待队列,如果在后面则直接被服务器接受请求。 - 为什么循环接收同一个client的数据,判断read()返回值用0?
当接收队列为空,且本端或对端调用shutdown或close连接,read()才会返回零。并不是接收队列为空就返回0。所以该函数能一直读到client退出输入循环。
附录
[1] 探讨read的返回值的三种情况
[2] Github