一 什么是套接字
套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发既可以在本地单机上进行,也可以跨网络进行。
二 套接字属性
套接字的特性由3个属性确定,它们是:域,类型和协议
1 套接字的域
域指定套接字通信中的网络介质,最常见的套接字域是AF_INET,它指的是Internet网络,其底层的协议——网际(IP)只有一个地址族,它使用一种特定的方式来指定网络中的计算机,即人们常说的IP地址。
AF_UNIX域是UNIX文件系统域,即使是一台还没联网的计算机上的套接字也可以使用这个域,这个域的底层协议是文件输入/输出,而它的地址就是文件名。
2 套接字类型
因特网协议提供了两种通信机制:流和数据报。
2.1 流套接字
流套接字提供的是一个有序,可靠,双向字节流的连接,因此,发送的数据可以确保不会丢失,复制或乱序到达,并且在这一过程中发生的错误也不会显示出来。大的消息将被分片,传输,再重组。
流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP(TCP代表是传输控制协议,IP代表是网际协议)连接实现的。
2.2 数据报套接字
数据报套接字由类型SOCK_DGRAM指定,不建立和维持一个连接。它对可以发送的数据报的长度有限制。数据报作为一个单独的网络消息被传输,它可能会丢失,复制或乱序到达。
数据报套接字是在AF_INET域中通过UDP/IP连接实现的,它提供的是一种无序的不可靠服务(UDP代表的是用户数据报协议)。
数据报适用于信息服务中的“单次”查询,它主要用来提供日常状态信息或执行低优先级的日志记录。基于数据报的服务器通常不保留连接信息。
三 套接字的连接过程
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, //指定协议族
int type, //指定这个套接字的通信类型
int protocol //指定使用的协议,将该参数设置为0表示默认协议
);
以下是协议族: AF_UNIX:UNIX域协议(文件系统本地套接字)
AF_INET:ARPA因特网协议(UNIX网络套接字)
AF_ISO:ISO标准协议
AF_NS:施乐网络系统协议
AF_IPX:Novell IPX协议
AF_APPLETALK:Appletalk DDS
socket系统调用返回一个描述符,当这个套接字连接到另一端的套接字后,我们就可以用read和write系统调用,通过这个描述符来在套接字上发送和接收数据了。
2 套接字地址
每个套接字域都有其自己的地址格式,对于AF_UNIX域套接字来说,它的地址有结构sockaddr_un来描述,该结构定义在头文件sys/un.h中
#include <sys/un.h>
struct sockaddr_un{ sa_family_t sun_family;//AF_UNIX
char sun_path[];//pathname,长度限制为108个字符
};
对于AF_INET域,套接字地址由结构sockaddr_in来指定,该结构定义在头文件netinet/in.h中, 一个AF_INET套接字由它的域,端口号和IP地址来完全确定。
#include <netinet/in.h>
struct sockaddr_in{ short int sin_family; //AF_INET;
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
};
IP地址结构in_addr被定义为:
struct in_addr{ unsigned long int s_addr; //使用inet_addr(const char *cp)函数将一个点分十进制的IP转换成一个长整数型数 };
3 命名套接字
通过给套接字命名,可以让创建的套接字被其他进程使用
#include <sys/socket.h> int bind(int socket, //创建的socket
const struct sockaddr *address, //套接字地址地址,在AF_INET域中则是struct sockaddr_in类型,在AF_UNIX域中则是struct sockaddr_un类型
size_t address_len //地址结构的长度
); //bind调用在成功时返回0,失败时返回-1并设置errno
bind系统调用把参数address中的地址分配给与文件描述符socket关联的为命名套接字,地址结构的长度由参数address_len传递。
errno的值 描述
EBADF 文件描述符无效
ENOTSOCK 文件描述符对应的不是一个套接字
EINVAL 文件描述符对应的是一个已命名的套接字
EADDRNOTAVAIL 地址不可用
EADDRINUSE 地址已经绑定了一个套接字
EACCESS 权限不足
ENOTDIR,ENAMETOOLONG 表明选择的文件不符合要求
4 创建套接字队列
创建套接字队列能够保存服务器未处理的请求
#include <sys/socket.h> int listen(int socket, //socket文件标识符
int backlog //用来设置队列中可以容纳的未处理连接的最大数目,超过这个数目,往后的连接将被拒绝,导致客户的连接请求失败。
); //listen函数在成功时返回0,失败时返回-1
5 客户请求连接
客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法
#include <sys/socket.h> int connect(int socket, //指定的套接字将连接到参数address指定的服务器套接字
const struct sockaddr* address, //这个和服务器端的bind操作部分一样,看步骤2,3
size_t address_len //指定address指向的结构的长度
); //成功时,connect调用返回0,失败时返回-1并设置errno
errno值 描述
EBADF 传递给socket参数的文件描述符无效
EALREADY 该套接字上已经有一个正在进行中的连接
ETIMEOUT 连接超时
ECONNREFUSED 连接请求被服务器拒绝
如果连接不能立刻建立,connect调用将阻塞一段超时时间,一旦这个超时时间到达,连接将被放弃,connect调用失败。但如果connect调用被一个信号中断,而该信号又得到了处理,connect调用还是会失败
connect的阻塞特性可以通过设置该文件描述符的O_NONBLOCK标志来改变。
int flags=fcntl(socket,F_GETFL,0); fcntl(socket,O_NONBLOCK|flags);
6 服务器接受连接
#include <sys/socket.h> int accept(int socket, //客户程序连接到由socket参数指定的套接字上
struct sockaddr *address, //客户的地址,如果我们不关心客户的地址,可以将address参数指定为空指针
size_t *address_len //客户地址的长度,如果客户地址的长度超过这个值,它将被截断。
);
accept系统调用只有当有客户程序试图连接到由socket参数指定的套接字上时才返回,这里的客户是指套接字队列中排在第一个的未处理的连接。
一般情况下,如果套接字队列中没有未处理的连接,accept将阻塞(程序将暂停)直到有客户建立连接为止,和请求连接一样,可以通过使用fcntl函数对套接字文件描述符设置O_NONBLOCK标志
当有未处理的客户连接时,accept函数将返回一个新的套接字文件描述符。
7 关闭连接
close系统用来结束套接字连接。
四 示例
服务器程序server.c
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h>
int main(){ int server_sockfd,client_sockfd; int server_len,client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; //为服务器创建一个未命名的套接字
server_sockfd=socket(AF_INET,SOCK_STREAM,0); //命名套接字
server_address.sin_family=AF_INET; server_address.sin_addr.s_addr=inet_addr("127.0.0.1"); server_address.sin_port=9734; server_len=sizeof(server_address); bind(server_sockfd,(struct sockaddr *)&server_address,server_len); //创建一个连接队列,开始等待客户进行连接
listen(server_sockfd,5); while(1){ char ch; printf("server waiting\n"); //接受一个连接
client_len=sizeof(client_address); client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_address,&client_len); //对client_sockfd套接字上的客户进行读写操作
read(client_sockfd,&ch,1); ch++; write(client_sockfd,&ch,1); close(client_sockfd); } }
客户程序client.c
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h>
int main(){ int sockfd; int len; struct sockaddr_in address; int result; char ch='A'; //为客户创建一个套接字
sockfd=socket(AF_INET,SOCK_STREAM,0); //命名套接字,与服务器保持一致
address.sin_family=AF_INET; address.sin_addr.s_addr=inet_addr("127.0.0.1"); address.sin_port=9734; len=sizeof(address); //将我们的套接字连接到服务器的套接字
result=connect(sockfd,(struct sockaddr*)&address,len); if(result==-1){ perror("oops:client1"); exit(1); } //现在就可以通过sockfd进行读写操作了
write(sockfd,&ch,1); read(sockfd,&ch,1); printf("char from server=%c\n",ch); close(sockfd); exit(0); }