网络编程实战 1
- Ethernet frame
- IP packet
- TCP segment
- Application message
三个层次
- 充分理解TCP/IP网络模型和协议
- 结合对协议的理解, 增强对异常情况的处理能力
- 可以写出支持大规模高并发的网络处理程序
TCP/IP 四层模型
MAC层的传输单位是帧(frame), IP层的传输单位是包(packet), TCP层的传输单位是段(segment), HTTP的传输单位是消息或报文(message).
TCP --> stream socket, 字节流套接字, SOCK_STREAM
UDP --> datagram socket, 数据包套接字, SOCK_DGRAM
OSI网络分层模型
OSI (开放式系统互联通信参考模型, open system interconnection reference model),
TCP/IP是一个纯软件的栈, 没有网络应有的网卡, 光缆等设备的位置. 而OSI填补了这一层次的缺失, 使得理论层面描述的网络更为完整.
socket是什么
客户端发起连接请求之前, 需要服务端初始化. 服务端首先初始化socket, 然后执行bind函数, 将服务能力绑定在约定好的地址和端口, 接着执行listen操作, 监听特定端口, 然后在accept函数处阻塞, 等待客户端连接请求的到来.
三次握手完成之后, 建立连接, 数据的传输就不再是单向的而是双向的.
通用套接字地址格式
/* POSIX.1g 规范规定了地址族为2字节的值. */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址 */
struct sockaddr {
sa_family_t sa_family; /* 地址族. 16-bit*/
char sa_data[14]; /* 具体的地址值 112-bit */
};
sa_family
指定为AF_LOCAL
表示本地地址, AF_INET
表示使用IPv4, AF_INET6
表示使用IPv6. AF_
表示的是Address Family
, PF_
表示的是Protocal Family
, 常用AF_XXX
初始化socket地址, PF_XXX
初始化socket.
IPv4套接字格式地址
/* IPV4套接字地址,32bit值. */
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
/* 描述IPV4的套接字地址格式 */
struct sockaddr_in
{
sa_family_t sin_family; /* 16-bit, IPv4为AF_INET */
in_port_t sin_port; /* 端口口 16-bit*/
struct in_addr sin_addr; /* Internet address. 32-bit */
/* 这里仅仅用作占位符,不做实际用处 */
unsigned char sin_zero[8];
};
服务端准备连接的过程
创建套接字
int socket(int domain, int type, int protocol)
domain
为 PF_INET
, PF_INET6
, PF_LOCAL
. type
对于TCP为SOCK_STREAM,
UDP为SOCK_DGRAM
.
绑定套接字和套接字地址
bind(int fd, sockaddr *addr, socklen_t len)
sockaddr *
可以看作是一个void *
类型的参数
struct sockaddr_in name;
bind(sock, (struct sockaddr *)&name, sizeof(name))
对于IPv4地址, 可按照如下方式设置通配地址:
struct sockaddr_in name;
name.sin_addr.s_addr = htonl(INADDR_ANY); // IPv4通配地址
让服务器处于可监听的状态
int listen(int sockfd, int backlog);
listen
函数可以将"主动"的套接字转换为"被动"套接字, 告诉操作系统内核该套接字是用于等待用户请求的. 参数backlog
为未完成连接队列的大小.
成功应答
int accept(int listen_sock_fd, struct sockaddr *cli_addr, socklen_t *addrlen)
其中参数cli_addr
和addrlen
是函数的返回值, cli_addr
是客户端的地址, addrlen
指明了地址的大小, 函数返回的是一个新的套接字, 代表了与客户端的连接.
Q: 为什么需要有监听套接字和已连接套接字?
A: 这是考虑到网络程序需要具有一定的并发性, 监听套接字需要一直存在才能够服务多个客户端. 而一个客户端和服务端连接成功后, 就会生成一个已连接套接字, 使用已连接套接字和客户进行通信处理. 如果对该客户的服务已经完成, 那么将释放这一个客户的连接.
客户端发起连接的过程
客户端建立套接字的方法和服务端大致相同. 不同之处是客户端需要调用connect
向服务端发起请求.
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
sockfd
是连接套接字, servaddr
和addrlen
代表指向套接字地址结构的指针和该结构的大小. 套接字地址结构需要有服务器的IP地址和端口号.
对于TCP套接字, 调用connect
函数将会引发TCP的3次握手过程. 函数会在建立连接成功或出错时返回.
以上就是阻塞式网络编程模型的服务端与客户端.
思考题
数据流从应用程序发送端一直到应用程序接收端, 总共经历多少次拷贝?
7次以上(中间还需要经过网络设备拷贝数据). 应用程序将数据送到发送缓冲区时, 调用send
或是write
方法, 如果缓存中没有空间, 系统调用就会失败或被阻塞. 这个动作是显式拷贝. 之后, 数据将按照TCP/IP的分层进行拷贝, 这些拷贝对我们而言是透明的.