前言

上一篇实现了一个简单web服务器, 他只能相应一次请求, 发送一条信息, 虽然简单, 但总算迈出了第一步, 我们的服务器功能还相当匮乏, 这一篇我们要详细学习相关函数/定义

1. 函数 socket()

int socket(int af, int type, int protocol);

作用: 创建套接字

参数1: af为IP地址类型: AF_INET(表示IPv4), AF_INET6(表示IPv6), PF_INET(=AF_INET), PF_INET6(=AF_INET6), 等

参数2: type: 数据传输方式/套接字类型: SOCK_STREAM(流格式套接字/面向连接的套接字), SOCK_DGRAM(数据报套接字/无连接的套接字, 等

参数3: protocol: IPPROTO_TCP(TCP), IPPTOTO_UDP(UDP), 0(如果设为0, 意思是使用的协议可以通过参数1和2推断出来, 比如tcp和udp)

return: 成功就返回新建的socket的文件描述符, 失败返回-1

2. 函数 bind()

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);

作用: 将指定了通信协议的套接字文件与自己的IP和端口绑定起来.
参数1: sock: socket文件描述符
参数2: addr: sockaddr结构体变量的指针
参数3: addrlen: addr变量的大小
return: 成功返回0, 失败返回-1

3. 结构体 sockaddr_in

1
2
3
4
5
6
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};

参数1: sin_family: 地址族
参数2: sin_port: 端口号, 理论取值065536, 但是01023一般由系统分配给特定服务程序, 所以尽量取一个大一点的值
参数3: sin_addr: 32位IP地址, in_addr结构体类型, 是为了适应旧版本代码, 具体往后看
参数4: sin_zero[8]: 没有意义, 一般填充为0

4. 结构体 in_addr

1
2
3
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};
  1. in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节.

  2. s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换:

    unsigned long ip = inet_addr("127.0.0.1");
  3. 这样设计是为了为了兼容前面的代码

5. 结构体 sockaddr

1
2
3
4
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
  1. sockaddr 和 sockaddr_in可以相互转换, 他们所占的字节数量是相同的.
  2. 可以认为sockaddr是通用的, 而sockaddr_in是专门存放IPv4地址的结构体, 定义时使用sockaddr_in, 使用时将它强制转换为sockaddr.

6. 函数 connect()

1
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);

各个参数的说明和 bind() 相同,不再赘述.
作用: 请求连接服务器

7. 函数 write() & read()

write():
1
ssize_t write(int fd, const void *buf, size_t nbytes);

参数1: fd: 要写入的文件的描述符
参数2: buf: 要写入的数据的缓冲区地址
参数3: nbytes: 要写入的数据的字节数
return:write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

size_t 是通过 typedef 声明的 unsigned int 类型;
ssize_t 在 "size_t" 前面加了一个"s",代表 signed,
即 ssize_t 是通过 typedef 声明的 signed int 类型。

注意:

  1. TCP协议独立于 write()函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()
1
ssize_t read(int fd, void *buf, size_t nbytes);

参数1: fd: 要读取的文件的描述符
参数2: buf: 要接受的数据的缓冲区地址
参数3: nbytes: 要读取的数据的字节数

注意:

  1. read()也是从输入缓冲区中读取数据,而不是直接从网络中读取.

8. 函数 linsten()

int listen(int sock, int backlog);

参数1: sock: 需要进入监听状态的套接字
参数2: backlog: 请求队列的最大长度, SOMAXCONN可以设为最大值.

  1. 当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue).
  2. 缓冲区满了, 客户端会收到ECONNREFUSED错误

9. accpet()

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

参数1: sock: 服务器端套接字
参数2: addr: sockaddr_in结构体变量
参数3: addrlen: addr的长度

9. 转换函数 htonl(), htons(), ntohl(), ntohs()

HBO和NBO的概念:
主机字节顺序HBO(Host Byte Order)
网络字节顺序NBO(Network Byte Order)
先转换再用

四个转换函数:
htonl()--"Host to Network Long"
htons()--"Host to Network Short"
ntohl()--"Network to Host Long"
ntohs()--"Network to Host Short"
INADDR_ANY 泛指本机
作用: 如果有多个网卡, 只需要管理一个套接字, **接受某端口传输的数据就可以了**

socket缓冲区

特性:

  1. I/O缓冲区在每个TCP套接字中单独存在;
  2. I/O缓冲区在创建套接字时自动生成;
  3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  4. 关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

1
2
3
4
5
6
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

//结果 8192

TCP套接字的阻塞模式

对于TCP套接字(默认情况下),当使用 write()发送数据时:

1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()函数继续写入数据。

2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()也会被阻塞,直到数据发送完毕缓冲区解锁,write() 才会被唤醒。

3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。

4) 直到所有数据被写入缓冲区 write() 才能返回。

当使用 read()读取数据时

1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。

2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()函数再次读取。

3) 直到读取到数据后 read()函数才会返回,否则就一直被阻塞。

这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性

尾声

不知不觉第三篇结束了, 扎实了基础之后, 下一篇让我们继续改进第二篇文章讲的web服务器程序