Linux 系统编程 —— socket 编程

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

字节序转换函数:(大小端)

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);			//主机字节序转换为网络字节序,32位长整型
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);			//网络字节序转换为主机字节序,32位长整型
uint16_t ntohs(uint16_t netshort);

socket地址的数据类型及相关函数

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6、UNIX Domain Socket,各种网络协议的地址格式并不相同,如下图所示:
在这里插入图片描述
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 * 类型表示,在传递参数之前要强制类型转换一下,例如:

bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

IPv4 的 sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换。

IP 地址格式转换函数:(点分十进制和in_addr)

#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

//字符串转in_addr的函数:
int inet_aton(const char *strptr, struct in_addr *addrptr);		//转换后的结果存放在 *addrptr 中,成功返回非0,失败返回0
in_addr_t inet_addr(const char *strptr);		//转换后的结果作为函数的返回值(无法处理255.255.255.255) 失败返回-1
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);		
  • strptr : 点分十进制的 IP 地址(字符串)
  • addrptr : in_addr 类型的 IP 地址

基于 TCP 协议的网络通信

基于TCP协议的客户端/服务器程序的一般流程:
在这里插入图片描述

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

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

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

socket API

socket函数:创建网络通信端口

#include <sys/socket.h>

int socket(int domain, int type,  int  protocol);
  • domain:用于通信的协议簇,IPv4 -> AF_INET,IPv6 -> AD_INET6,unix -> AF_UNIX
  • type:对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。
  • protocol:介绍从略,指定为0即可。

成功返回一个文件描述符,失败返回-1

bind函数:将一个套接字和一个地址绑定在一起

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd,  struct  sockaddr  *my_addr,  socklen_t addrlen);

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

  • sockfd:用于网络通讯的文件描述符
  • my_addr:struct sockaddr 类型的结构体
    my_addr 参数的初始化:
bzero(&servaddr, sizeof(my_addr));		//清零
my_addr.sin_family = AF_INET;		//协议名 IPv4 -> AD_INET
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);	//INADDR_ANY:表示本地的任意IP地址
//因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
my_addr.sin_port = htons(8000);		//端口号设为8000
  • addrlen:结构体的长度

成功返回0,失败返回-1

listen函数:监听申请的连接

#include <sys/socket.h>

int listen(int sockfd, int backlog);

listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。

  • sockfd:套接口的描述字
  • backlog:未完成连接队列的最大长度

成功返回0,失败返回-1。

accept函数:接受客户端的连接请求

#include <sys/types.h>
#include <sys/socket.h>

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

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

  • sockfd:套接口的描述字
  • cliaddr:传出参数,accept()返回时传出客户端的地址和端口号。如果为NULL,表示不关心客户端的地址。
  • addrlen:传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。由于addrlen是传入传出参数,每次调用accept()之前应该重新赋初值。

成功返回一个新的套接字描述符,它代表的是和客户端的新的连接,可以把它理解成是一个客户端的socket,这个socket包含的是客户端的ip和port信息 。(当然这个new_socket会从sockfd中继承 服务器的ip和port信息,两种都有了),而参数中的sockfd 包含的是服务器的ip和port信息 ;出错返回-1。

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

connect 函数:客户端连接服务器

#include <sys/types.h>   
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
  • sockfd:客户端的套接口描述字
  • servaddr:服务器的sockaddr
  • addrlen:结构体的大小

成功返回0,出错返回-1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值