在linux系统编程中网络编程是使用socket(套接字),socket这个词可以表示很多概念:
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP
地址+端口号”就称为socket。在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连接的一对一关系。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API,本节的主要内容是socket API。
1、网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
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代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#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位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
2、IP地址转换函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
只能处理IPv4的ip地址,不可重入函数,注意参数是struct in_addr
3、sockaddr数据结构
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示
4、TCP/UDP对比
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接;
2、TCP提供可靠的服务,也就是说通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付;
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等);
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信;
5、TCP首部开销20字节;UDP的首部开销小,只有8字节;
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道;
5、端口号的作用
一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过一个IP地址来实现。那么主机是怎么样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务关系是一对多的关系。
实际上主机是通过“IP地址+端口号”来区分不同的服务的。端口提供了一种访问通道,服务器一般都是通过知名端口号来识别的。例如对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
6、socket通信过程
7、相关api介绍
连接协议(socket):
函数原型:int socket(int domain, int type, int protocol);
参数1int domain:指明所使用的协议,通常为AF_INET,表示互联网协议族(TCP/IP协议族);
(AF_INET—IPv4因特网域、AF_INET6—IPv6因特网域、AF_UNIX—Unix域、AF_ROUTE—路由套接字、AF_KEY—密钥套接字、AF_UNSPEC—未指定)
参数2 int type:指定socket的类型;
(SOCK_STREAM:流式套接字提供可靠的、面向连接的通信流;使用TCP协议,保证了数据传输的正确性和顺序性;SOCK_DGRAM:数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,而且不保证是可靠的、无差错的。它使用UDP协议;SOCK_RAM:允许程序使用底层协议,原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用不便,用于协议的开发)
参数3int protocol:通常赋值0;
(0:选择type类型对应的默认协议;IPPROTO_TCP—TCP协议;IPPROTO_UDP—UDP协议;IPPROTO_SCTP—SCTP协议;IPPROTO_TIPC—TIPC协议)
成功返回该socket的文件描述符,否则返回-1;
绑定IP地址和端口号(bind):
函数原型:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数1int sockfd:是一个socket描述符;
参数2const struct sockaddr *addr:结构体指针,包含协议族、端口号、IP地址等;
参数3socklen_t addrlen:结构体大小;
成功返回0,否则返回-1;
这里涉及到IP地址转换问题:我们人眼看到的是字符串,我们要把IP地址转换为网络能识别的格式:
int inet_aton(const char *straddr,struct in_addr *addrp); //字符串转网络格式
char* inet_ntoa(struct in_addr inaddr); //网络格式转字符串
监听设置函数(listen):
函数原型:int listen(int sockfd, int backlog);
参数1int sockfd:服务器端socket描述符;
参数2int backlog:指定在请求队列中允许的最大请求数;
成功返回0,否则返回-1;
服务器接收客户端连接(accept):
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数1int sockfd:服务器端socket描述符;
参数2struct sockaddr *addr:返回已连接的客户端的协议地址;
参数3socklen_t *addrlen:客户端地址长度;
成功返回一个新的套接字描述符,即已连接的套接字描述符,否则返回-1;
客户端连接服务器(connect):
函数原型:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数1int sockfd:目的服务器的socket描述符;
参数2const struct sockaddr *addr:服务器端的IP地址和端口号的地址结构体指针;
参数3socklen_t addrlen:地址长度;
成功返回0,否则返回-1;
数据收发:
函数原型:
1.ssize_t read(int fd, void *buf, size_t count); //读数据
2.ssize_t write(int fd, const void *buf, size_t count); //写数据
8、Socket服务器和客户端的开发步骤
服务器开发:1.创建套接字(socket)— 2.为套接字添加信息(IP地址和端口号)(bind)— 3.监听网络连接(listen)— 4.监听到有客户端接入,接受一个连接(accept)— 5.数据交互(read、write)— 6.关闭套接字,断开连接(close)
客户端开发:1.创建套接字(socket)— 2.连接服务器(connect)— 3.数据交互(read、write)— 4.关闭套接字,断开连接(close)