网络编程(一)
参考文献:
本文为学习极客时间的《网络编程实战》笔记,课程链接为
https://time.geekbang.org/column/intro/214
SOCKET通信总体框架图
1. 套接字和地址
1.1 socket的定义
是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
1.2 socket的表示形式
socket = (IP地址:端口号),点分十进制的IP地址,后跟端口号,中间用分号或者冒号隔开。
1.3 socket的类型
- SOCKET_STREAM, 流套接字:提供面向连接的、可靠的数据传输服务。采用了TCP传输层协议;
- SOCKET_DGRAM, 数据报套接字:提供一种无连接的服务。采用UDP传输层协议;
- SOCKET_RAW, 原始套接字:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议数据,数据报套接字只能读取UDP协议数据。因此访问其他协议必须使用原始套接字。
1.4 套接字地址格式
套接字地址分为4个类型,包括通用地址格式、IPv4地址格式、IPv6地址格式和本地地址格式。
- 通用地址格式
sa_family表示socket地址族,占2个字节,指示对什么地址格式进行解释和保存,常见有以下三种类型:
- AF_LOCAL: 表示的是本地地址,一般适用于本地socket通信;
- AF_INET: 表示采用了IPv4地址;
- AF_INET6: 表示采用了IPv6地址;
sa_data标识具体地址,占14个字节。
- IPv4地址格式
sin_family字段表示socket地址族,占2个字节;
sin_port字段表示端口号,占2个字节;
sin_addr字段表示Internet address,因特网地址,占4个字节;
- IPv6地址格式
sin6_family字段表示协议族,占2个字节;
sin6_port字段表示端口号,占2个字节;
sin6_addr字段表示IPv6地址,占16个字节;
- 本地地址格式
几种地址的格式的对比
2.使用套接字建立连接
建立连接包括服务端准备连接和客户端发起连接两个流程
2.1 服务端准备连接
服务端连接准备,包括创建SOCKET对象,将SOCKET对象和SOCKET地址进行绑定,监听SOCKET连接请求。
- 创建SOCKET
int socket(int domain, int type, int protocol);
domain指定采用什么地址族,比如PF_INET, PF_INET6, PF_LOCAL;
type指定socket类型,比如SOCKET_STREAM, SOCKET_DGRAM, SOCKET_RAW;
protocol 原来用于指定协议,现已废弃。
- bind
将创建的socket对象和socket地址绑定,客户端通过socket地址就能够找到对应的socket对象,并进行通信。
bind(int fd, sockaddr* addr, socklen_t len);
第1个参数fd 文件描述符,socket对象本质是文件;
第2个参数addr 通用socket地址格式,实际可传入IPv4 IPv6 Local地址格式;
第3个参数len 传入地址长度,bind()会根据len长度判读传入参数addr是那种地址类型。
bind()的一个例子
/* 创建socket */
int sock = socket(PF_INET, SOCK_STREAM, 0);
/* 构造socket地址 */
struct sockaddr_in name;
name.sin_family = AF_INET; /* IPv4 */
name.sin_port = htons(port); /* 指定端口号*/
name.sin_addr.s_addr = htonl(INADDR_ANY); /*通配地址*/
/* 将sock与地址进行绑定*/
bind(sock, (struct sockaddr *) &name, sizeof(name));
通配地址:不关注具体到那个ID地址,只要客户端发起的连接 只要本机网卡的IP地址就行;
端口号:客户端必须准确地了解。
- listen
listen() 是让socket对象进入监听状态,告诉操作系统内核说“这个套接字是用来等等用户请求的”,操作系统内核会为此做好接受用户请求的一切准备。函数原型如下
int listen(int socketfd, int backlog);
第一个参数socketfd为套接字描述符;
第二个参数backlog为未完成连接队列的大小,决定了可以接受并发连接数目的大小。
- accept
服务端监听到客户端发起的连接时,成功应答并建立连接,操作系统内核需要通知到应用程序,让应用程序感知到这个事件。
accept的作用就是在建立连接之后,操作系统内核和应用程序之间的桥梁。
int accept(int listensockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
第1个参数listensockfd,是处于“监听”状态的套接字;
第2个参数cliaddr,是获取发起连接的客户端的IP地址;
第3个参数addrlen,是客户端IP地址的长度;
返回值是已连接套接字描述符。
2.2 客户端发起连接
客户端发起连接通过connect函数
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
第1个参数sockfd是连接套接字;
第2个参数servaddr是服务端套接字地址指针;
第3个参数addrlen是服务端套接字地址的长度。
客户端在调用connect()发起连接之前不必非得进行bind(),因为如果需要的话,系统内核会确定源IP地址,并按照一定算法选择一个临时端口作为源端口。
3. 发送和读取数据
3.1 发送缓存区
TCP建立之后,操作系统内核为每个连接建立发送缓冲区。发送缓冲区的大小可以通过套接字选项来设定。当应用程序调用write()函数时,会将数据写入发送缓存区,此时还没有将数据发送到对端。操作系统内核会像流水线一样不断地将发送缓存区的数据传输给服务端应用程序。调用write()函数将需要发送的数据都写入发送缓存区时,返回实际发送的数据长度。
3.2 发送数据
ssize_t write (int socketfd, const void* buffer, size_t size)
ssize_t send(int socketfd, const void* buffer, size_t size, int flags)
ssize_t sendmsg(int socketfd, const struct msghdr* msg, int flags)
三个函数都可以实现数据发送,其中主要的参数描述,
第1个参数:文件描述符;
第2和3个参数:传入待发送的数据;
返回类型是实际发送的数据长度
3.3 读取数据
ssize_t read(int socketfd, void* buffer, size_t size)
int recvmsg(int socketfd, msgaddr* msg, unsigned int flags)
第1个参数是文件描述符;
返回类型是实际读取的数据长度。
3.4 socket-write-read 实践
完成了基于socket的网络编程demo,采用c++和cmake,支持client向server发送指定字节长度的数据,server接受该数据并打印在屏幕上,源代码放在github上了。
https://github.com/chaochon/socket-write-and-read