Socket在所有网络操作系统和网络应用程序中都是必不可少的,它是网络通信中应用进程和网络协议之间的接口。在Linux操作系统中,socket属于文件系统的一部分,网络通信可以看作是对文件的读取。这就使得用户对网络的控制像对文件的控制一样方便。
要了解socket就必须要了解一些基本的概念,如:套接口、网络编程的结构等。下面分别讲述这些概念。
一、基本概念:
1.套接口:
简单地说,套接口就是一种使用UNIX系统中的文件描述符和系统进程通信的一种方法。因为在UNIX系统中,所有的I/O操作都是通过读写文件描述符而产生的。文件描述符就是一个和打开的文件相关连的整数。但文件可以是一个网络连接、一个FIFO、一个管道、一个终端、一个真正存储在磁盘上的文件或者UNIX系统中的任何其他的东西。所以,如果你希望通过Internet和其他的程序进行通信,你只有通过文件描述符。使用系统调用socket(),你可以得到socket()描述符。然后你可以使用send()和recv()调用而与其他的程序通信。你也可以使用一般的文件操作来调用read()和write()而与其他的程序进行通信,但send()和recv()调用可以提供一种更好的数据通信的控制手段。
2.Internet套接口
有两种最常用的Internet套接口,“数据流套接口”和“数据报套接口”,我们用“SOCK_STREAM”和“SOCK_DGRAM”分别代表上面两种套接口。数据报套接口有时也叫做“无连接的套接口”。数据流套接口是可靠的双向连接的通信数据流。如果你在套接口中以“ 1, 2”的顺序放入两个数据,它们在另一端也会以“1, 2”的顺序到达。它们也可以被认为是无错误的传输。经常使用的telnet应用程序就是使用数据流套接口的一个例子。使用HTTP的WWW浏览器也使用数据流套接口来读取网页。事实上,如果你使用telnet登录到一个WWW站点的80端口,然后键入“GET网页名”,你将可以得到这个HTML页。数据流套接口使用TCP得到这种高质量的数据传输。数据报套接口使用UDP,所以数据报的顺序是没有保障的。数据报是按一种应答的方式进行数据传输的。
3.数据结构
下面我们要讨论使用套接口编写程序可能用到的数据结构。
struct sockaddr
{
unsigned short int sa_family;
char sa_data[14];
};
sa_family 为调用socket()时的domain参数,即AF_xxxx值。
sa_data最多使用14个字符长度。
此sockaddr结构会因使用不同的socket domain而有不同结构定义,例如使用AF_INET domain,其socketaddr结构定义便为:
struct socketaddr_in
{
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct i
n_addr
{
uint32_t s_addr;
};
sin_family 即为sa_family
sin_port 为使用的port编号
sin_addr.s_addr 为IP地址
sin_zero 填充0以保持与struct sockaddr大小一致。
这个数据结构使得使用其中的各个元素更为方便。要注意的是sin_zero应该使用bzero() 或者memset()而设置为全0。另外,一个指向sockaddr_in数据结构的指针可以投射到一个指向数据结构sockaddr的指针,反之亦然。
4.网络字节顺序
一个网络可能由不同的体系结构的CPU组成,这些不同体系的CPU使用的字节顺序不同,有的CPU使用big_endian(大端,在存储器中高字节存储在后),有的CPU使用little_endian(小端,在存储器中高字节存储在前)。因此为了在网络间能够进行数据交换,需要对这些不同的字节顺序进行处理。
下面是几个字节顺序转换函数:
htons():表示“Host to Network Short”,把主机地址字节顺序转向网络字节顺序(对短整型数据操作)。
htonl():表示“Host to Network Long”,把主机地址字节顺序转向网络字节顺序(对长整型数据操作)。
ntohs():表示“Network to Host Short”,把网络字节顺序转向主机地址字节顺序(对短整型数据操作)。
ntohl():表示“Network to Host Short”,把网络字节顺序转向主机地址字节顺序(对长整型数据操作)。
二、SOCKET通信常用API
1.socket(建立一个socket通信) 相关函数 accept,bind,connect,listen
表头文件
#include #include 定义函数 int socket(int domain,int type,int protocol); 函数说明 socket()用来建立一个新的socket,也就是向系统注册,通知系统建立一通信端口。参数domain 指定使用何种的地址类型,完整的定义在/usr/include/bits/socket.h 内,底下是常见的协议: PF_UNIX/PF_LOCAL/AF_UNIX/AF_LOCAL UNIX 进程通信协议 PF_INET?AF_INET Ipv4网络协议 PF_INET6/AF_INET6 Ipv6 网络协议 PF_IPX/AF_IPX IPX-Novell协议 PF_NETLINK/AF_NETLINK 核心用户接口装置 PF_X25/AF_X25 ITU-T X.25/ISO-8208 协议 PF_AX25/AF_AX25 业余无线AX.25协议 PF_ATMPVC/AF_ATMPVC 存取原始ATM PVCs PF_APPLETALK/AF_APPLETALK appletalk(DDP)协议 PF_PACKET/AF_PACKET 初级封包接口 参数 type有下列几种数值: SOCK_STREAM 提供双向连续且可信赖的数据流,即TCP。支持 OOB 机制,在所有数据传送前必须使用connect()来建立连线状态。 SOCK_DGRAM 使用不连续不可信赖的数据包连接 SOCK_SEQPACKET 提供连续可信赖的数据包连接 SOCK_RAW 提供原始网络协议存取 SOCK_RDM 提供可信赖的数据包连接 SOCK_PACKET 提供和网络驱动程序直接通信。 protocol用来指定socket所使用的传输协议编号,通常此参考不用管它,设为0即可。 返回值 成功则返回socket处理代码,失败返回-1。 错误代码 E PROTONOSUPPORT 参数domain指定的类型不支持参数type或protocol指定的协议 ENFILE 核心内存不足,无法建立新的socket结构 EMFILE 进程文件表溢出,无法再建立新的socket EACCESS 权限不足,无法建立type或protocol指定的协议 ENOBUFS/ENOMEM 内存不足 EINVAL 参数domain/type/protocol不合法 2.bind(对socket定位) 相关函数 socket,accept,connect,listen 表头文件 #include #include 定义函数 int bind(int sockfd,struct sockaddr * my_addr,int addrlen); 函数说明 bind()用来设置给参数sockfd的socket一个名称。 参数 addrlen为sockaddr的结构长度。 返回值 成功则返回0,失败返回-1,错误原因存于errno中。 错误代码 EBADF 参数sockfd 非合法socket处理代码。 EACCESS 权限不足 ENOTSOCK 参数sockfd为一文件描述词,非socket。 3. connect(建立socket连线) 相关函数 socket,bind,listen 表头文件 #include #include 定义函数 int connect (int sockfd,struct sockaddr * serv_addr,int addrlen); 函数说明 connect()用来将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。结构sockaddr请参考bind()。参数addrlen为sockaddr的结构长度。 返回值 成功则返回0,失败返回-1,错误原因存于errno中。 错误代码 EBADF 参数sockfd 非合法socket处理代码 EFAULT 参数serv_addr指针指向无法存取的内存空间 ENOTSOCK 参数sockfd为一文件描述词,非socket。 EISCONN 参数sockfd的socket已是连线状态 ECONNREFUSED 连线要求被server端拒绝。 ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。 ENETUNREACH 无法传送数据包至指定的主机。 EAFNOSUPPORT sockaddr结构的sa_family不正确。 EALREADY socket为不可阻断且先前的连线操作还未完成。 范例 /* 此程序会连线TCP server,并将键盘输入的字符串传送给server。*/ #include #include #include #include #include #include #include #define PORT 1234 #define SERVER_IP “127.0.0.1” int main(void) { int s; struct sockaddr_in addr; char buffer[256]; if((s = socket(AF_INET,SOCK_STREAM,0))<0) { perror(“socket”); exit(1); } /* 填写sockaddr_in结构*/ bzero(&addr,sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port=htons(PORT); addr.sin_addr.s_addr = inet_addr(SERVER_IP); /* 尝试连线*/ if(connect(s,&addr,sizeof(addr))<0) { perror(“connect”); exit(1); } /* 接收由server端传来的信息*/ recv(s,buffer,sizeof(buffer),0); printf(“%s\ ”,buffer); while(1) { bzero(buffer,sizeof(buffer)); /* 从标准输入设备取得字符串*/ read(STDIN_FILENO,buffer,sizeof(buffer)); /* 将字符串传给serv er端*/ if(send(s,buffer,sizeof(buffer),0)<0) { perror(“send”); exit(1); } } } 执行 $ ./connect Welcome to server! hi I am client! /*键盘输入*/ /* 4. accept(接受socket连线) 相关函数 socket,bind,listen,connect 表头文件 #include #include 定义函数 int accept(int s,struct sockaddr * addr,int * addrlen); 函数说明 accept()用来接受参数s的socket连线。参数s的socket必需先经bind()、listen()函数处理过,当有连线进来时 accept()会返回一个新的socket处理代码,往后的数据传送与读取就是经由新的socket处理,而原来参数s的socket能继续使用 accept()来接受新的连线要求。连线成功时,参数addr所指的结构会被系统填入远程主机的地址数据,参数addrlen为scokaddr的结构 长度。关于结构sockaddr的定义请参考bind()。 返回值 成功则返回新的socket处理代码,失败返回-1,错误原因存于errno中。 错误代码 EBADF 参数s 非合法socket处理代码。 EFAULT 参数addr指针指向无法存取的内存空间。 ENOTSOCK 参数s为一文件描述词,非socket。 EOPNOTSUPP 指定的socket并非SOCK_STREAM。 EPERM 防火墙拒绝此连线。 ENOBUFS 系统的缓冲内存不足。 ENOMEM 核心内存不足。 5. listen(等待连接) 相关函数 socket,bind,accept,connect 表头文件 #include 定义函数 int listen(int s,int backlog); 函数说明 listen()用来等待参数s 的socket连线。参数backlog指定同时能处理的最大连接要求,如果连接数目达此上限则client端将收到ECONNREFUSED的错误。 Listen()并未开始接收连线,只是设置socket为listen模式,真正接收client端连线的是accept()。通常listen()会 在socket(),bind()之后调用,接着才调用accept()。 返回值 成功则返回0,失败返回-1,错误原因存于errno 附加说明 listen()只适用SOCK_STREAM或SOCK_SEQPACKET的socket类型。如果socket为AF_INET则参数backlog 最大值可设至128。 错误代码 EBADF 参数sockfd非合法socket处理代码 EACCESS 权限不足 EOPNOTSUPP 指定的socket并未支援listen模式。 范例 #include #include #include #include #include #define PORT 1234 #define MAXSOCKFD 10 int main(void) { int sockfd,newsockfd,is_connected[MAXSOCKFD],fd; struct sockaddr_in addr; int addr_len = sizeof(struct sockaddr_in); fd_set readfds; char buffer[256]; char msg[ ] =”Welcome to server!”; if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0) { perror(“socket”); exit(1); } bzero(&addr,sizeof(addr)); addr.sin_family =AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(soc kfd,&addr,sizeof(addr))<0) { perror(“connect”); exit(1); } if(listen(sockfd,3)<0) { perror(“listen”); exit(1); } for(fd=0;fd while(1) { FD_ZERO(&readfds); FD_SET(sockfd,&readfds); for(fd=0;fd if(is_connected[fd]) FD_SET(fd,&readfds); } if(!select(MAXSOCKFD,&readfds,NULL,NULL,NULL)) continue; for(fd=0;fd if(FD_ISSET(fd,&readfds)) { if(sockfd = =fd) { if((newsockfd = accept (sockfd,&addr,&addr_len))<0) perror(“accept”); write(newsockfd,msg,sizeof(msg)); is_connected[newsockfd] =1; printf(“cnnect from %s\ ”,inet_ntoa(addr.sin_addr)); } else { bzero(buffer,sizeof(buffer)); if(read(fd,buffer,sizeof(buffer))<=0) { printf(“connect closed.\ ”); is_connected[fd]=0; close(fd); } else printf(“%s”,buffer); } } } } 执行 $ ./listen connect from 127.0.0.1 hi I am client connected closed. 6. select() select()系统调用可以使进程检测同时等待的多个I/O设备,当没有设备准备好时,select()阻塞,其中任一设备准备好时,select()就返回。 select()的调用形式为: #include #include int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout); 参数: select 的第一个参数是文件描述符集中要被检测的比特数,这个值必须至少比待检测的最大文件描述符大1;参数readfds指定了被读监控的文件描述符集;参数 writefds指定了被写监控的文件描述符集;而参数exceptfds指定了被例外条件监控的文件描述符集。 参数timeout起了定时器的作用:到了指定的时间,无论是否有设备准备好,都返回调用。timeval的结构定义如下: struct timeval{ long tv_sec; //表示几秒 long tv_usec; //表示几微妙 } timeout取不同的值,该调用就表现不同的性质: 1.timeout为0,调用立即返回; 2.timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪; 3.timeout为正整数,就是一般的定时器。 select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。select的返回值有如下情况: 1.正常情况下返回就绪的文件描述符个数; 2.经过了timeout时长后仍无设备准备好,返回值为0; 3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。 4.如果出错,返回-1并设置相应的errno。 8.send(经socket传送数据) 相关函数 sendto,sendmsg,recv,recvfrom,socket 表头文件 #include #include 定义函数 int send(int s,const void * msg,int len,unsigned int falgs); 函数说明 send()用来将数据由指定的socket 传给对方主机。参数s为已建立好连接的socket。参数msg指向欲连线的数据内容,参数len则为数据长度。参数flags一般设0,其他数值定义如下 MSG_OOB 传送的数据以out-of-band 送出。 MSG_DONTROUTE 取消路由表查询 MSG_DONTWAIT 设置为不可阻断运作 MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断。 返回值 成功则返回实际传送出去的字符数,失败返回-1。错误原因存于errno 错误代码 EBADF 参数s 非合法的socket处理代码。 EFAULT 参数中有一指针指向无法存取的内存空间 ENOTSOCK 参数s为一文件描述词,非socket。 EINTR 被信号所中断。 EAGAIN 此操作会令进程阻断,但参数s的socket为不可阻断。 ENOBUFS 系统的缓冲内存不足 ENOMEM 核心内存不足 EINVAL 传给系统调用的参数不正确。 9.recv(经socket接收数据) 相关函数 recvfrom,recvmsg,send,sendto,socket 表头文件 #include #include 定义函数 int recv(int s,void *buf,int len,unsigned int flags); 函数说明 recv()用来接收远端主机经指定的socket传来的数据,并把数据存到由参数buf 指向的内存空间,参数len为可接收数据的最大长度。 参数 flags一般设0。其他数值定义如下: MSG_OOB 接收以out-of-band 送出的数据。 MSG_PEEK 返回来的数据并不会在系统内删除,如果再调用recv()会返回相同的数据内容。 MSG_WAITALL强迫接收到len大小的数据后才能返回,除非有错误或信号产生。 MSG_NOSIGNAL此操作不愿被SIGPIPE信号中断返回值成功则返回接收到的字符数,失败返回-1,错误原因存于errno中。 错误代码 EBADF 参数s非合法的socket处理代码 EFAULT 参数中有一指针指向无法存取的内存空间 ENOTSOCK 参数s为一文件描述词,非socket。 EINTR 被信号所中断 EAGAIN 此动作会令进程阻断,但参数s的socket为不可阻断 ENOBUFS 系统的缓冲内存不足。 ENOMEM 核心内存不足 EINVAL 传给系统调用的参数不正确。 10.read(由已打开的文件读取数据) 相关函数 readdir,write,fcntl,close,lseek,readlink,fread 表头文件 #include 定义函数 ssize_t read(int fd,void * buf ,size_t count); 函数说明 read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。若参数count为0,则read()不会有作用并返回0。返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。 附加说明 如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数 少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。 错误代码 EINTR 此调用被信号所中断。 EAGAIN 当使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。 EBADF 参数fd 非有效的文件描述词,或该文件已关闭。 11.write(将数据写入已打开的文件内) 相关函数 open,read,fcntl,close,lseek,sync,fsync,fwrite 表头文件 #include 定义函数 ssize_t write (int fd,const void * buf,size_t count); 函数说明 write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。 返回值 如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。 错误代码 EINTR 此调用被信号所中断。 EAGAIN 当使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。 EADF 参数fd非有效的文件描述词,或该文件已关闭。 补充:文件描述符 系统提供了4个宏对描述符集进行操作: #include #include void FD_SET(int fd, fd_set *fdset); void FD_CLR(int fd, fd_set *fdset); void FD_ISSET(int fd, fd_set *fdset); void FD_ZERO(fd_set *fdset); 宏FD_SET 设置文件描述符集fdset中对应于文件描述符fd的位(设置为1),宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为 0),宏FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)。使用这3个宏在调用select前设置描述符屏蔽位,在调用 select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。 过去,描述符集被作为一个整数位屏蔽码得到实现,但是这种实现对于多于32个的文件描述符将无法工作。描述符集现在通常用整数数组中的位域表示,数组元素的每一位对应一个文件描述符。例如,一个整数占32位,那么整数数组的第一个元素代表文件描述符0到31,数组的第二个元素代表文件描述符32到63,以此类推。宏FD_SET设置整数数组中对应于fd文件描述符的位为1,宏FD_CLR设置整数数组中对应于fd文件描述符的位为0,宏FD_ZERO设置整数数组中的所有位都为0。假设执行如下程序后: #include #include fd_set readset; FD_ZERO(&readset); FD_SET(5, &readset); FD_SET(33, &readset); 再执行如下程序后: FD_CLR(5, &readset); 通常,操作系统通过宏FD_SETSIZE来声明在一个进程中select所能操作的文件描述符的最大数目。例如: 在4.4BSD的头文件中我们可以看到: #ifndef FD_SETSIZE #define FD_SETSIZE 1024 #endif 在红帽Linux的头文件 #define __FD_SETSIZE 1024 以及在头文件 #include #define FD_SETSIZE __FD_SETSIZE 既定义FD_SETSIZE为1024,一个整数占4个字节,既32位,那么就是用包含32个元素的整数数组来表示文件描述符集。我们可以在头文件中修改这个值来改变select使用的文件描述符集的大小,但是必须重新编译内核才能使修改后的值有效。当前版本的unix操作系统没有FD_SETSIZE 的最大值,通常只受内存以及系统管理上的。 我们明白了文件描述符集的实现机制之后,就可对其进行灵活运用。(以下程序在红帽Linux 6.0下运行通过,函数fd_isempty用于判断文件描述符集是否为空;函数fd_fetch取出文件描述符集中的所有文件描述符) #include #include #include #include struct my_fd_set{ fd_set fs; //定义文件描述符集fs unsigned int nconnect; //文件描述符集fs中文件描述符的个数 unsigned int nmaxfd; //文件描述符集fs中最大的文件描述符 }; /* 函数fd_isempty用于判断文件描述符集是否为空,为空返回1,不为空则返回0 */ int fd_isempty(struct my_fd_set *pfs) { int i; /* 文件描述符集fd_set是通过整数数组来实现的,所以定义整数数组myset的元素个数为文件描述符集fd_set所占内存空间的字节数除以整数所占内存空间的字节数。 */ unsigned int myset[sizeof(fd_set) / sizeof(int)]; /* 把文件描述符集pfs->fs 拷贝到数组myset */ memcpy(myset, &pfs->fs, sizeof(fd_set)); for(i = 0; i < sizeof(fd_set) / sizeof(int); i++) /* 如果myset的某个元素不为0,说明文件描述符集不为空,则函数返回0 */ if (myset[i]) return 0; return 1; /* 如果myset的所有元素都为0,说明文件描述符集为空,则函数返回1 */ } /* 函数fd_fetch对文件描述符集进行位操作,把为1的位换算成相应的文件描述符,然后就可对其进行I/O操作 */ void fd_fetch(struct my_fd_set *pfs) { struct my_fd_set *tempset; //定义一个临时的结构指针 unsigned int myset[sizeof(fd_set)/sizeof(unsigned int)]; unsigned int i, nbit, nfind, ntemp; tempset = pfs; memcpy(myset, &tempset->fs, sizeof(fd_set)); /* 把最大的文件描述符maxfd除以整数所占的位数,得出maxfd在文件描述符集中相应的位对应于整数数组myset的相应元素的下标,目的是为了减少检索的次数 */ nfind = tempset->nmaxfd / (sizeof(int)*8); for (i = 0; i <= nfind; i++) { /* 如果数组myset的某个元素为0,说明这个元素所对应的文件描述符集的32位全为0,则继续判断下一元素。*/ if (myset[i] == 0) continue; /* 如果数组myset的某个元素不为0,说明这个元素所对应的文件描述符集的32位中有为1的,把myset[i]赋值给临时变量ntemp,对ntemp进行位运算,把为1的位换算成相应的文件描述符 */ ntemp = myset[i]; /* nbit记录整数的二进制位数,对ntemp从低到高位进行&1运算,直到整数的最高位,或直到文件描述符集中文件描述符的个数等于0 */ for (nbit = 0; tempset->nconnect && (nbit < sizeof(int)*8); nbit++) { if (ntemp & 1) { /* 如果某位为1,则可得到对应的文件描述符为nbit + 32*I,然后我们可对其进行I/O操作。这里我只是做了简单的显示。*/ printf("i = %d, nbit = %d, The file description is %d\下载本文