网络编程-一个简单的echo程序(1)

前言

在《网络编程-一个简单的echo程序(0)》中已经对程序整体有了宏观的认识,本文将抽丝剥茧,逐步深入了解echo程序。

程序代码

由于代码内容较多,具体代码可访问《网络编程-一个简单的echo程序(0)》或者访问:
https://www.yanbinghu.com/2019/07/07/40135.html

数据结构与函数详解

既然要详细了解echo程序,就必须对其中用到的一些数据结构和接口有所了解。在echo程序中,我们主要用到了以下的数据结构或函数:

  • htons/ntohs
  • inet_pton/inet_ntop
  • sockaddr_in
  • socket
  • bind
  • listen
  • connect
  • accept

当然需要清楚的是,网络编程中用到的数据结构或函数远不止上面提到的这些,但这些都是最基本的。下面的解释都基于echo程序,多数函数都使用默认的阻塞模式。

htons/ntohs

htons/ntohs这两个宏分别用于将本地字节序转为网络字节序和将网络字节序转为本地字节序。关于字节序,本文不展开介绍,可以参考《谈一谈字节序的问题》,如何判断当前机器的字节序,也是面试中经常问到的题目。

inet_pton/inet_ntop

inet_pton/inet_ntop分别用于将字符串ip地址转为4字节大小的无符号整型和将无符号整型转换为ip地址字符串。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//来源:公众号【编程珠玑】网站:https://www.yanbinghu.com
#include<stdio.h>
#include <arpa/inet.h>
int main(void)
{
char ip[16] = "192.168.0.1";
struct in_addr addr;
inet_pton(AF_INET, ip, &addr);
printf("addr is %x\n",addr);

addr.s_addr = 0x153a8c0;
inet_ntop(AF_INET,&addr,ip,sizeof(ip));
printf("ip is %s",ip);
return 0;
}

运行结果:

1
2
addr is 100a8c0                                                                 
ip is 192.168.83.1

从运行结果中可以清晰看到两者之间的转换。需要注意的是,inet_pton/inet_ntop对IPV4和IPV6地址都适用。

sockaddr_in

sockaddr_in是IPV4套接字地址结构,它在不同系统中具体定义可能有所不同:

1
2
3
4
5
6
struct sockaddr_in{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

但它们都包含三个基本的成员:

  • sin_family 协议族
  • sin_port 协议端口
  • sin_addr 协议地址

协议族通常有以下几种类型:

  • AF_INET IPV4协议
  • AF_INET6 IPV6协议
  • AF_LOCAL Unix域协议
  • AF_ROUTE 路由套接字
  • AF_KEY 秘钥套接字

而目前echo程序中用到的是IPV4协议,因此选择了AF_INET。

而sin_port就比较容易理解了,它是一个16比特大小的端口,但是由于它的信息需要在网络中传输,因此需要使用前面介绍的htons进行字节序的转换。

sin_addr用4字节存储ip地址,如果是形如127.0.0.1的地址,需要通过inet_pton函数将其转换为struct in_addr类型。

socket—确定协议族和套接字类型

调用socket函数是执行网络I/O之前必须做的一件事情。通过socket函数指定了本次网络通信的协议族,套接字类型,调用成功后,会返回一个非负的套接字描述符,否则返回-1,具体失败原因,被存放于全局变量errno。它和文件描述类似,只不过此时它还不能进行正常的网络读写。
socket函数相关信息如下:

1
2
#include<sys/socket.h>
int socket(int family,int type,int protocol);

其中family就是在介绍sockaddr_in中提到的协议族。

type通常有以下几个值:

  • SOCK_STREAM 字节流套接字
  • SOCK_DGRA 数据报套接字
  • SOCK_RAW 原始套接字
  • SOCK_SEQPACKET 有序分组套接字
  • SOCK_PACKET 分组套接字

需要注意的是:

  • TCP仅支持字节流套接字
  • UDP仅支持数据报套接字
  • SCTP支持字节流套接字和数据报套接字

protocol通常指以下几种:

  • IPPROPO_TCP TCP协议
  • IPPROPO_UDP UDP协议
  • IPPROPO_SCTP SCTP协议

通常来说,一种传输协议只支持一种套接字,此时protocol可以为0,系统会选择其对应的协议类型;否则的话,需要指定protocol的值。在当前echo程序中,type为SOCK_STREAM,我们的protocol值为0,因此使用的就是TCP协议。

我们通过一个简单的例子,观察这个套接字描述符:

1
2
3
4
5
6
7
8
9
10
11
//testSocket.c
//来源:公众号【编程珠玑】网站:https://www.yanbinghu.com
#include<stdio.h>
#include <arpa/inet.h>
#include<unistd.h>
int main(void)
{
int socktfd = socket(AF_INET,SOCK_STREAM,0);
sleep(20);
return 0;
}

在一个终端运行testSocket,在另外一个终端找到该程序的pid,并查看打开的文件描述符:

1
2
3
4
5
6
7
8
$ pidof testSocket
5903
$ ls -l /proc/5903/fd/
total 0
lrwx------ 1 hyb hyb 64 7月 8 19:59 0 -> /dev/pts/6
lrwx------ 1 hyb hyb 64 7月 8 19:59 1 -> /dev/pts/6
lrwx------ 1 hyb hyb 64 7月 8 19:59 2 -> /dev/pts/6
lrwx------ 1 hyb hyb 64 7月 8 19:59 3 -> socket:[62182]

还记得那句话吗:linux下一切皆文件。

bind—指定套接字地址信息

调用socket函数之后已经确定了协议族和传输协议,但是还没有确定本地协议,即套接字地址信息。bind函数描述如下:

1
2
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

sockfd是前面调用socket函数返回的套接字描述符,用于将协议地址绑定到指定套接字中去,返回0表明成功,-1表示失败,具体失败原因,被存放于全局变量errno。addr是套接字地址,它并不是我们前面所看到的sockaddr_in类型,而是struct sockaddr,因为struct sockaddr是通用类型,不仅适用于IPV4套接字地址,也需要适用于IPV6套接字地址。

addr中的ip地址可以为0(INADDR_ANY),表示使用通配地址;而端口为0,表示由内核分配一个临时端口。服务器需要被客户端连接,因此其端口通常都是确定的,不会选择一个临时端口。

但是在客户端其ip地址和端口并非需要确切知道,因此客户端常常不绑定端口。在我们的echo程序中,我们也没有在客户端调用bind函数。

listen—监听客户端连接

listen函数用于将前面得到的套接字变为一个被动套接字,即可用于接受来自客户端的连接。描述如下:

1
2
#include<sys/socket.h>
int listen(int sockfd,int backlog);

返回0表明成功,-1表明失败,具体失败原因,被存放于全局变量errno。sockfd就是socket函数调用返回的套接字描述符,而backlog指明了连接队列的大小,即完成和还未完成TCP三次握手的连接总和。如果这个队列满了,服务器就不会理会新的连接请求。还记得在《网络编程-从TCP连接的建立说起》中提到的SYN攻击吗?

connect—建立连接

connect函数在客户端调用,它用来与服务端建立连接。描述如下:

1
2
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

返回0表明成功,-1表明失,具体失败原因,被存放于全局变量errno。connect函数的参数与bind函数一样,这里就不多做解释了,只不过addr指明的是远端协议地址。如果本次连接是TCP协议,则connect函数调用将会发起TCP的三次握手

accept—接受来自客户端的连接

accept函数在服务端调用,它用于接受来自客户端的连接,从已完成连接队列返回一个已完成连接。描述如下:

1
2
#include<sys/socket.h>
int accept(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

成功返回非负套接字描述符,失败返回-1,具体失败原因,被存放于全局变量errno。需要注意的是accept函数参数类型和数量与connect函数一致,但是含义不同,addr用于获取客户端的套接字地址信息,如果不关心客户端的协议地址,那么该参数可为NULL。

另外需要注意的是,它的返回值是一个非负的套接字描述符,这个套接字描述符是已连接套接字描述符,而其参数sockfd是监听套接字描述符。一个服务器通常一直有且只有一个监听套接字描述符,但通常会有多个已连接套接字描述符。还记得在上一篇中问到的吗?为什么客户端连接到服务端后,服务端有一个处于LISTEN状态,还有一个处于ESTABLISHED状态吗?

通过已连接套接字描述符就可以对其进行数据的读写了。

小结

本文主要对echo程序中用到的一些数据结构和函数进行了介绍,但没有涉及具体的异常场景,后面的文章将根据实际情况来看看其具体应用。本文常用接口总结如下:

接口 作用 成功 失败 调用者
socket 确定协议族和套接字类型 套接字描述符 -1 客户端/服务端
bind 确定套接字地址 0 -1 [客户端]/服务端
listen 套接字转为被动套接字 0 -1 服务端
connect 建立连接 0 -1 客户端
accept 接受连接 套接字描述符 -1 服务端

网络编程

参考书籍

  • 《Unix网络编程》
  • 《TCP/IP协议详解:卷一》
守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!