一、基础
1.1 套接字定义
套接字是网络数据传输的软件设备。我们可以通过套接字完成数据传输。
1.2 相关函数
网络编程中接受连接请求的套接字创建过程如下:
- 调用socket函数创建套接字
- 调用bind函数分配IP和端口号
- 调用listen函数转换为可接受请求状态
- 调用accept受理连接请求
- Linux平台相关函数
#include <sys/socket.h> int socket(int domain, int type, int protocol); 成功返回文件描述符,失败返回-1 int bind(int socketfd, struct sockadr* myaddr, socklen_t addrlen); int listen(int socketfd, int backlog); int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen); int connect(int sockdf, struct sockaddr* serv_addr, socklen_t addrlen); 成功返回0,失败返回-1
- Windows平台相关函数
#include <winsock2.h> //需要链接ws2_32.lib //初始化 int WSAStartup(WORD wVersionRequested,LPWSADATA lpwSAData); 成功返回0,失败返回错误代码 wVersionRequested 使用的Winsock版本信息 lpwSAData WSADATA结构体变量的地址 MAKEWORD(1,2); //宏函数,主版本为1,副版本2,返回0x0201 //注销 int WSACleanup(); 成功返回0,失败返回SOCKET_ERROR //套接字 SOCKET socket(int af, int type, int protocol); 成功时返回套接字句柄,失败返回INVALID_SOCKET int bind(SOCKET s, const struct sockaddr* name, int namelen); 成功返回0,失败返回SOCKET_ERROR int listen(SOCKET s, int backlog); 成功返回0,失败返回SOCKET_ERROR SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen); 成功时返回套接字句柄,失败返回INVALID_SOCKET int connect(SOCKET s, const struct sockaddr* name, int namelen); 成功返回0,失败返回SOCKET_ERROR int closesocket(SOCKET s); 成功返回0,失败返回SOCKET_ERROR
1.3 类型和设置
1.3.1 协议(Protocol)
- 协议族分类
- PF_INET IPV4协议族
- PF_INET6 IPV6协议族
- PF_LOCAL 本地通信的Unix协议族
- PF_PACKET 底层套接字的协议族
- PF_IPX IPX Novell协议族
通过套接字的第一个参数传递使用的协议分类信息,domain决定使用的protocol。
- 套接字类型
类型指的是套接字的数据传输方式,决定了协议族不能决定数据传输方式,如PF_INET可能存在多种数据传输方式。-
SOCK_STREAM(面向连接的套接字)
面向连接的套接字具有以下特点:- 传输过程中数据不会消失
- 按序传输数据
- 传输的数据不存在数据边界
收发数据的内部有缓冲(字节数组)。套接字传输的数据保存到该数组。在数组容量内,可能在充满缓冲后一次读取,也可能分多次读取。如果缓冲被填满,传输套接字将停止传输。
SOCK_DGRAM(面向消息的套接字)
面向消息的套接字特点: 不可靠的、不按序传递、以数据的高速传递为目的的套接字。
-
1.4 地址族和字节序
1.4.1 地址族
IP是网络协议的简写,是为了收发网络数据而分配给计算机的值。只要有IP就能像数据发送到目标计算机,但无法发送给最终的应用程序。
端口号是为了区分程序中创建的套接字而分配给套接字的序号。NIC通过IP向计算机内部发生数据,操作系统利用端口号将数据分配给套接字。
IP地址分为IPV4(4字节)和IPV6(16字节)两类。IPV6是为了解决201年前后IP地址耗尽而提出的标准。IPV4分为A、B、C、D四类,由网络ID和主机主机ID构成。 IP网络数据传输时,先基于网络地址(网络ID)把数据发送到网络。网络收到数据后,根据主机ID将数据发送到目标计算机。
地址分类
- A类地址首字节 0-127,首位以0开始
- B类地址首字节128-191,首位以10开始
- C类地址首字节192-223,首位以110开始
端口号
- 端口号由16位构成,可分配端口号为0-65535
- 知名端口号:0-1023。一般分配给特定程序。
1.4.2 地址信息表示
struct sockaddr_in
{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
}
in_addr结构体声明为uint32_t,只需要当作32位整数。
sin_zero是使结构体与socd_addr保持一致而插入的成员。
之所以使用sockaddr_in,而不直接使用sockaddr,原因在于sockaddr结构体,如下:
struct sock_addr
{
sa_family_t sin_family;
char sa_data[14];
}
sa_data中包含地址和端口号,其余部分为0。直接填充这些信息会比较麻烦,于是有了sockaddr_in,最后转换为sockaddr型结构体。
1.4.3 字节序
大端序: 高位字节放在地位地址
小端序: 高位字节放在高位地址
在通过网络传输数据时约定统一方式,即网络字节序。网络字节序是大端序。
字节序转换
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
//h--->host字节序
//n-->network字节序
//s-->2字节short,常用于端口号转换
//l-->4字节,常用于IP转换
地址字符串的字节序转换
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string); //点分十进制字符串---> 大端序整数值
int inet_aton(const char* string, struct in_addr* addr); //string转换结果保存到addr地址值中
char* inet_ntoa(struct in_addr adr); //转换为字符串形式。长期保存时,需要对char*内容进行复制
网络地址初始化
struct sockaddr_in addr;
char* server_ip = "211.217.168.11"; //ip字符串
char* serv_port = "9190"; //端口号
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(server_ip);
addr.sin_port = htons(server_port);
INADDR_ANY
利用INADDR_ANY分配Ip地址,将自动获取计算机的地址,若计算机有多个IP地址,则可以从不同IP、相同端口号中接收数据。服务器一般采用这种方式。
1.5 域名和网络地址
将容易记、易表达的域名分配并取代IP地址。域名是赋予服务器端的虚拟地址,而非实际地址。通过DNS请求转换地址。若DNS服务器无法解析,会询问其他DNS服务器,并提供给用户。
//
#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
structhostent* gethostbyaddr(const char* addr, socklen_t len, int family);
1.6套接字 多种可选项
1.6.1 可选项
套接字可选项是分层的。IPPROTO_IP是IP协议相关选项,IPPROTO_TCP是tcp协议相关可选项,SOL_SOCKET是套接字相关通用可选项。
设置可选项
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
sock 套接字文件描述符
level 协议层
optname 可选项名字
optval 保持查看结果的缓冲地址
optlen 缓冲大小
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);
SO_TYPE 查看套接字类型
SO_SNDBUF/SO_RCVBUF 输入输出缓冲区大小
SO_REUSEADDR :默认为0(假),即无法分配Time-wait状态下的套接字端口号, 为1(真)可将Time-wait状态下的套接字端口号重新分配给新的套接字
套接字四次挥手后并非立即清除,二十经过一段时间的Time-wait状态。先断开连接(发送FIN消息)的主机才经过Time-wait状态。因此服务器先断开连接,无法立即重新运行。
TCP_NODEALY:设置为1时,禁用Nagle算法。
tcp套接字默认使用Nagle算法(只有收到前一数据的ACK时,才发送下一数据)交换数据,因此最大限度的进行缓冲,直到收到ACK。为了提高网络传输效率,必须使用Nagle算法。但在网络流量未受到太大影响时,不使用Nagle算法会更快。如大文件数据。
1.7 进程
1.8 IO复用与IO函数
无论连接多少客户端,提供服务的进程只有一个。select是最具代表性的服务端复用方法。
1.9 线程
1.10 信号处理
二、TCP
2.1 定义
IP层解决数据传输中的路径选择问题。TCP和UDP利用IP层提供的路径信息完成实际的数据传输。
2.2 服务端
TCP服务端函数调用通常遵循如下顺序:
- socket -- 创建套接字
- bind -- 分配套接字地址
- listen -- 等待连接请求状态
- accept -- 允许连接
- read/wrote -- 数据交换
- close -- 断开连接
listen后,若有新的连接请求,应该按序处理。受理请求意味着进入可接受数据的状态。
accept受理连接请求。其内部产生用于数据IO的套接字,并返回其文件描述符。
客户端通过connect发起连接请求,当服务端接收连接请求或者发生异常中断连接请求时返回。服务端接收连接请求只是将请求记录加入等待队列,并不表示调用accept,不表示立即进行数据交换。
2.3 迭代服务端
通过插入循环语句,反复调用accept函数来持续受理客户端连接请求的方式。
2.4 迭代回声服务/客户端
TCP数据传输不存在边界。服务端通过1次write函数传输数据,但是如果数据太大,可能会分成多个数据包发送。另外,客户端也可以未完全收到数据就read。解决方法是提前确定接收数据的大小。在无法预知数据大小的情况下,需要应用层协议的定义。收发数据的过程中定义号规则以表示数据的边界。
2.5 半关闭
IO缓冲特性:
- I/O缓冲在每个套接字中单独存在
- 在创建套接字时自动生成
- 关闭套接字后,也会继续传输输出缓冲中的遗留数据
- 关闭套接字,会丢失输入缓冲中的数据
Linux的close和Windows的closescket意味着完全断开连接,即无法传输和接受数据。这样可能存在主机A发送数据后断开连接,无法继续接收B的数据。因此,Half-close产生,即只关闭1个流。
半关闭函数
#include <sys/socket.h>
int shutdown(int sock, int howto);
//howto为断开方式信息
断开方式如下:
- SHUT_RD 断开输入流
- SHUT_WR 断开输出流
- SHUT_RWDR 同时断开输入输出流
shutdown进行半关闭,同时发送EOF。
2.6 并发服务器
2.7 IO分离
2.8 select服务端
2.8.1 select调用过程
- 设置文件描述符、指定监视范围、设置超时
- 调用select函数
- 查看调用结果
2.9 多线程服务端
三、 UDP
3.1 定义
UDP提供不可靠数据传输。UDP不提供流控制机制。根据端口号将传到主机的数据交付给最终的UDP套接字。
TCP比UDP慢主要由于:
- 收发数据前后进行的连接设置和清楚操作
- 收发数据过程中为保证可靠性而添加的流控制
3.2 UDP服务端/客户端
3.2.1 UDP数据IO函数
#include <sys/cosket.h>
ssize_t sendto(int sock, void* buf, size_t bnytes, int flags, struct sockaddr* to, socklen_t addrlen)
ssize_t recvfrom(int sock, void* buf, size_t nbytes, int flags, struct sockaddr* from, socklen_t* addrlen)
3.2.2 套接字地址分配
sendto前完成地址分配。如果sendto时未分配地址信息,首次调用时自动分配IP和端口号,分配结果保留到程序结束。IP为主机IP,端口号为任意未分配端口。
此外,也可以用bind分配ip和端口号。
3.3 数据传输特性和connect
存在数据边界。通过snedto传输数据过程如下:
- 向套接字注册目标IP和端口号
- 传输数据
- 删除注册信息
未注册目标信息的套接字称为未连接套接字。反之称为connected套接字。
向同一目标主机进行长时间通信时,将UDP套接字变为connected套接字会提高效率。传输过程中,1和3占传输的1/3时间。