Socket编程框架

1. Socket地址数据类型及相关函数
sockaddr数据结构
  • IPv4和IPv6的地址格式定义在netinet/in.h中
  • IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址
  • IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。
  • UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。
  • 各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和UNIX Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如
struct sockaddr_in servaddr;/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); 


本文主要实现基于IPV4的Socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址,但是实践中也通常使用点分十进制的字符串表示IP地址,以下函数可以在字符串类型表示和in_addr 类型表示之间进行转换

  • 字符串转in_addr的函数
#include 
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr); 
  • in_addr转字符串的函数
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); 

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

2. 基于TCP/IP协议的网络程序以及常用的 Socket API 函数接口介绍
2.1 C/S框架一般流程图
TCP协议通讯流程
步骤:

Step1. 建立连接过程

  • 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态
  • 客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答
  • 服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

Step2. 数据传输过程

  • 建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

Step3. 关闭连接过程

  • 如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

[注]
在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段

2.2 C/S网络通信实例

通过最简单的C/S程序的实例来学习socket API


server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端

server.c

上面程序中用到的最基本的Socket API函数接口介绍,这些函数都在sys/socket.h

1.int socket(int family, int type, int protocol);

1.1 功能:
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果调用出错,则返回 -1.

1.2 参数:

  • family:对于IPv4family参数指定为AF_INET,其他的也就是上面介绍的地址类型
  • type:对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。
  • protocol:参数的介绍从略,指定为0即可。

2.int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

2.1 背景&功能:
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。
bind()作用是将参数sockfdmyaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号

2.2 参数:

  • sockfd : 文件描述符
  • struct sockaddr *struct sockaddr *是一个通用指针类型(上面有提及),myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度

struct sockaddr *myaddr参数常见初始化方式:
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

解释:首先将整个结构体清零;然后设置地址类型为AF_INET;网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为8000。

3. int listen(int sockfd, int backlog);

3.1 功能:
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

3.2 参数:

  • sockfd
  • backlog:最多允许有backlog个客户端处于连接等待状态

4.int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

4.1 背景&功能:
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。

4.2 参数

  • sockfd
  • cliaddr
  • addrlen

client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接受服务器返回的字符串并打印

client.c

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦

1.int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)

1.1 背景&功能
客户端需要调用connect()连接服务器,connectbind的参数形式一致,区别在于bind的参数是自己的地址,**而connect的参数是对方的地址。connect()成功返回0,出错返回-1。


2.3 错误处理与读写控制

上面的C/S实例不仅功能简单,而且几乎没有什么错误处理,但是,系统调用不能保证每次都成功,必须进行必要的出错处理,这样一方面可以保证程序的逻辑正常,另一方面可以迅速得到故障信息。

补充哦


UNIX Domain Socket IPC

Socket API原本是为网络通讯设计,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络通讯是为不可靠的通讯设计的。UNIX Domian Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。

UNIX Domain Socket是全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,比如X Window服务器和GUI程序之间就是通过UNIX DomainSocket通讯的。

使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。

UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。


参考

[Linux_C编程一站式学习·Ch37·Socket编程]
socket,connect,select

你可能感兴趣的:(Socket编程框架)