在前面我们讲了TCP/IP、TCP和UDP的一些基本知识,但是协议只有一套,而我们系统多个TCP连接或多个应用程序进程必须通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。
套接口可以说是网络编程中一个非常重要的概念,linux以文件的形式实现套接口,与套接口相应的文件属于sockfs特殊文件系统,创建一个套接口就是在sockfs中创建一个特殊文件,并建立起为实现套接口功能的相关数据结构。换句话说,对每一个新创建的BSD套接口,linux内核都将在sockfs特殊文件系统中创建一个新的inode。描述套接口的数据结构是socket,将在后面给
套接字简介
在前面我们讲了TCP/IP、TCP和UDP的一些基本知识,但是协议只有一套,而我们系统多个TCP连接或多个应用程序进程必须通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。
套接字就是支持TCP/IP网络通信的基本操作单元,是我们进行TCP/IP进行通信的接口。
linux以文件的形式实现套接口,与套接口相应的文件属于sockfs特殊文件系统,创建一个套接口就是在sockfs中创建一个特殊文件,并建立起为实现套接口功能的相关数据结构。换句话说,对每一个新创建的套接字,linux内核都将在sockfs特殊文件系统中创建一个新的inode。
套接字Socket看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。套接字Socket是连接应用程序和网络驱动程序的桥梁,套接字Socket在应用程序中创建,通过绑定与网络驱动建立关系。此后,应用程序送给套接字Socket的数据,由套接字Socket交给网络驱动程序向网络上发送出去。计算机从网络上收到与该套接字Socket绑定IP地址和端口号相关的数据后,由网络驱动程序交给Socket,应用程序便可从该Socket中提取接收到的数据,网络应用程序就是这样通过Socket进行数据的发送与接收的。
操作系统区分不同应用程序进程间的网络通信和连接,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。
Socket=Ipaddress+TCP/UDP+port
Socket原意是 “插座”。通过将这3个参数结合起来,与一个“插座”Socket绑定,应用层就可以和传输层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
套接字连接的过程如同(客户)打一个电话到一个大公司,接线员(服务器进程)接听电话并把它转接到你要找的部门,然后再从那里转到你要找的人(服务器套接字),然后接线员(服务器进程)再继续转接其它(客户)的电话。
套接字有本地套接字和网络套接字两种。本地套接字的名字是Linux文件系统中的文件名,一般放在/tmp或/usr/tmp目录中;网络套接字的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。
参考信息
Linux网络套接字
套接字类型
常用的TCP/IP协议的3种套接字类型如下所示。
流套接字(SOCK_STREAM):
流套接字用于提供面向连接、可靠的数据传输服务。看到这个我们想到了什么,是不是TCP
该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。
数据报套接字(SOCK_DGRAM)
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
原始套接字(SOCK_RAW)
原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
重要数据结构
下面是在网络编程中比较重要的几个数据结构
表示套接口的数据结构struct socket
用户使用socket系统调用编写应用程序时,通过一个数字来表示一个socket,所有的操作都在该数字上进行,这个数字称为套接字描述符。在系统调用 的实现函数里,这个数字就会被映射成一个表示socket的结构体,该结构体保存了该socket的所有属性和数据。
套接口是由socket数据结构代表的,形式如下
struct socket
{
socket_state state; /*指明套接口的连接状态,一个套接口的连接状态可以有以下几种
套接口是空闲的,还没有进行相应的端口及地址的绑定;还没有连接;正在连接中;已经连接;正在解除连接。*/
unsignedlong flags;
structproto_ops ops; /*指明可对套接口进行的各种操作*/
structinode inode; /*指向sockfs文件系统中的相应inode*/
structfasync_struct *fasync_list; /* Asynchronous wake up list */
structfile *file; /*指向sockfs文件系统中的相应文件 */
structsock sk; /*任何协议族都有其特定的套接口特性,该域就指向特定协议族的套接口对
象。*/
wait_queue_head_t wait;
short type;
unsignedchar passcred;
};
更加详细的关于socket的解释请参照
struct socket 结构详解
或者
http://anders0913.iteye.com/blog/411986
描述套接口通用地址的数据结构struct sockaddr
由于历史的缘故,在bind、connect等系统调用中,特定于协议的套接口地址结构指针都要强制转换成该通用的套接口地址结构指针。结构形式如下:
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx, sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族。
*/
char sa_data[14]; /*14 bytes of protocol address, sa_data是14字节协议地址。*/
};
此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构
描述因特网地址结构的数据结构struct sockaddr_in
每个套接字域都有自己的地址格式。
AF_UNIX 域套接字格式
#include <sys/un.h>
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
AF_INET 域套接字格式IPV4
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
IP地址是由4个字节组成的一个32位的值。
#include <netinet/in.h>
struct sockaddr_in
{
short sin_family;/* Addressfamily一般来说AF_INET(地址族)PF_INET(协议族) */
unsigned short sin_port;/* Portnumber(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字) */
struct in_addr sin_addr;/* Internetaddress存储IP地址 */
unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)]; /* Samesizeasstructsockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/
};
AF_INET6 域套接字格式IPV6
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
对于应用程序来说,套接字就和文件描述符一样,并且通过一个唯一的整数值来区分。
基本接口函数
创建套接字的函数socket( )
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
函数原型声明
#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, /* 创建的套接字的协议族, AF_XXX */
int type, /* 创建的套接字的类型, SOCK_XXX */
int protocol);/* 创建的套接字的协议,与type有关 */
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
参数介绍
domain,即协议域,又称为协议族(family)
常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type,指定socket类型
常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol,故名思意,就是指定协议
常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。
当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口
绑定地址函数bind()[服务器使用]
通过socket调用创建的套接字必须经过命名(绑定地址)后才能使用。
bind系统调用把addr中的地址分配给与描述符socket关联的未命名套接字,地址结构的长度由addr_len指定。 addr和addr_len因地址族(AF_UNIX、AF_INET等)的不同而不同,bind调用时需要将指向特定地址结构的指针转化为指向通用地址的指针,即(struct sockaddr *)。
函数原型声明
#include <sys/socket.h>
int bind( int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
参数
sockfd,即socket描述字
它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr,待绑定的地址
一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,分别指向前面的。
参数addr为通用地址结构,一般只需提供固定的端口号,即如下设置
struct sockaddr_in server_add; /* 服务器信息 */
server_add.sin_family = AF_INET; /* IPV4地址格式 */
server_add.sin_addr.s_addr = htonl(INADDR_ANY); /* 接受任意IP地址的客户连接 */
server_add.sin_port = htons(port); /* port为服务器端指定的端口号,unsigned short int类型 */
server_len = sizeof(server_add);
bind(server_sockfd, (struct sockaddr*)&server_add, server_len);
注意,此函数一般只由服务器使用,通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
主机字节序与网络字节序
主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。
标准的大端模式(Big-Endian)和L小端模式(Little-Endian)的定义如下
小端模式Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
大端模式Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用Big-Endian
4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。即大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
关于主机字节序和网络字节序的问题,我们会在后续的博文中继续说明,但是我还是想在这里强调一下,因为太关键了,因为问题曾经坑了一代又一代的程序猿。。。
服务器listen()和客户端connect()
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket。
如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
listen函数原型声明
int listen( int sockfd,
int backlog);
listen参数介绍
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
服务器accpet设置模板代码
参数addr指向的socketadd地址结构用来存放将要连接到的客户的地址,只有accept成功返回时才有效。如果不关心客户地址,可以将addr参数指定为空指针;
int client_sockfd;
struct sockaddr_in client_add;
clinet_len = sizeof(client_add);
client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_add, &client_len);
connect函数原型声明
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect参数介绍
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
客户端connect设置模板代码
struct sockaddr_in server_add;
server_add.sin_family = AF_INET;
serevr_add.sin_addr.s_addr = inet_addr("***.***.***.***");
//服务器地址,无需htonl转换,因为inet_addr已定义为网络字节序
server_add.sin_addr.sin_port = htons(port); //port,int型变量,与服务器端相同的端口号
len = sizeof(server_add);
connect(sockfd, (struct sockaddr *)&server_add, len); //可根据返回值判断连接状态
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连接。
参数解释
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
read()、write()等函数
调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read( ) / write( )
recv( ) / send( )
readv( ) / writev()
recvmsg( ) / sendmsg( )
recvfrom( ) / sendto( )
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
而我们写程序很多人都会使用下面这两个send/recv函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd,
const void *buf,
size_t len,
int flags,
const struct sockaddr *dest_addr,
socklen_t addrlen);
ssize_t recvfrom(int sockfd,
void *buf,
size_t len,
int flags,
struct sockaddr *src_addr,
socklen_t *addrlen);
ssize_t sendmsg(int sockfd,
const struct msghdr *msg,
int flags);
ssize_t recvmsg(int sockfd,
struct msghdr *msg,
int flags);
3.6、close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
close函数
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意 close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
总结
套接字通信建立过程
服务器端
socket == 创建服务器监听套接字
服务器应用程序用系统调用socket创建一个套接字。 它是系统分配给服务器进程的类似文件描述符的资源。
bind == 绑定服务器监听信息到套接字
服务器进程用系统调用bind命名套接字。 然后服务器进程就开始等待客户连接到这个命名套接字。
listen == 开始监听,接收客户端的TCP连接
accept 从listen所维护的队列中取出一条已连接的TCP,返回该连接的socket描述字
服务器通过系统调用accept来接受客户的连接。 accept会创建一个不同于命名套接字的新套接字来与这个特定客户进行通信,而命名套接字则被保留下来继续处理其他客户的连接请求。
注意listen和accept是面向连接的套接字才会有的,正常的无连接通信在bind之后,服务器就会阻塞一直到接收到客户端发来的数据
close == 关闭打开着的套接字
最后关闭套接字,关闭打开的套接字文件描述符号
TCP服务器
UDP服务器
客户端
socket == 创建客户端连接套接字
调用socket创建一个未命名套接字。
connect == 向指定服务器发起连接请求
调用connect与服务器建立连接,将服务器的命名套接字作为一个地址。
然后服务器客户端在连接socket描述字上进行消息通信
close == 关闭打开着的套接字
TCP客户端
UDP服务器