Linux C语言程序设计(十七)——Socket编程的基础解析

时间:2021-10-24 10:25:40

1、socket概念

socket这个词可以表示很多概念:

1)在TCP/IP协议中, “IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程, “IP地址+端口号”就称为socket。

2)在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。 socket本身有“插座”的意思,因此用来描述网络连接的一对一关系。

3)TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socketAPI。

我们这里,主要介绍TCP协议的函数接口。


2、网络字节序

网络字节序的示意图如下:

Linux C语言程序设计(十七)——Socket编程的基础解析

        内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。如果主机是大端字节序的,发送和接收都不需要做转换,否则需要转换。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

这些函数名很好记, h表示host, n表示network, l表示32位长整数, s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。


3、套接字地址结构

Socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX Domain Socket。

3.1 IPv4套接字

IPv4套接字地址结构通常也称为“网际套接字地址结构”,它以sockaddr_in命名,定义在<netinet/in.h>头文件中。

struct in_addr专门用来存储IP地址,对于IPv4来说,IP地址为32位无符号整数。

struct in_addr {
unsigned long s_addr;
}

IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址。下边给出它的定义:

struct sockaddr_in {                     /* in表示Internet */
unsigned short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
char sin_zero[8]; /* 填充0(保持与struct sockaddr 一样大小) */
};

3.2 IPv6套接字

IPv6地址用sockaddr_in6结构体表示,包括16位端口号、 128位IP地址和一些控制字段。定义如下:

struct in6_addr {
uint8_t s6_addr[16]; /* 128-bit IPv6 address */
/* network byte ordered */
}

#define SIN6_LEN /* required for compile-time tests */

struct sockaddr_in6 {
uint8_t sin6_len; /* length of this struct (28) */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* transport layer port# */
/* network byte ordered */
uint32_t sin6_flowinfo; /* flow information, undefined */
struct in6_addr sin6_addr; /* IPv6 address */
/* network byte ordered */
uint32_t sin6_scope_id; /* set of interfaces for a scope */
}

3.3 Socket API

        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));

struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};

4、Socket基本操作

4.1 Socket函数

        socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

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

4.2 bind函数

        通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

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

sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。

addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。


4.3 listen()、connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

        listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

        connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。


4.4 accept()函数

        TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

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

        accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。


4.5 read()、write()等函数

服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!


4.6 close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。


5、字节序转换函数

        下图说明了网络字节序与小端字节序、大端字节序的对照关系。字节转换主要是针对整形进行转换,字符型由于是单字节,所以不存在这个问题。整形字节序转换函数原型及其说明如下表所示:

Linux C语言程序设计(十七)——Socket编程的基础解析

字符串转in_addr的函数:

#include <arpa/inet.h>
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。


6、备注说明

6.1 大端小端

1)小端就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

2)大端就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:

  A. 大端模式:

  低地址 -----------------> 高地址

  0x12  |  0x34  |  0x56  |  0x78


  B. 小端模式:

  低地址 ------------------> 高地址

  0x78  |  0x56  |  0x34  |  0x12


6.2 套接字类型

流式套接字(SOCK_STREAM):提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。

数据报式套接字(SOCK_DGRAM):提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。

原始套接字(SOCK_RAW):