三 Socket套接字
目的:将TCP/IP协议相关软件移植到UNIX类系统中。
与套接字相关的函数被包含在头文件sys/socket.h
中
1.Socket套接字简介
任何用户在通信之前,首先要申请一个Socket号,同时要知道对方的Socket号。向对方发出请求时,对方主机如果开机且可以接受连接请求,则双方可通话。
Socket的通信机制与电话交换机制非常相似。Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则没有办法建立联系并通信。
每一个Socket都用一个半相关描述:
{协议,本地地址,本地端口}
一个完整的Socket则用一个相关描述:
{协议,本地地址,本地端口,远程地址,远程端口}
每个Socket有一个本地唯一的Socket号,由操作系统分配。套接字有3种类型:
- 流式套接字:提供可靠的、面向连接的通信流。可用于Telnet远程连接,www服务等需要使数据顺序传递的应用,使用TCP协议保证数据传输的可靠性
- 数据包套接字:定义一种无连接服务,数据通过相互独立的报文进行传输,是无序的并且不保证可靠性。使用UDP协议,数据只是简单地传送到对方
- 原始套接字:允许对底层协议如IP或ICPM直接访问,主要用于新的网络协议实现的测试等。用于一些协议的开发,可以进行比较底层的操作。
流式套接字工作原理如下图:
数据套接字工作原理如下图:
2.创建套接字
建立套接字的系统调用为socket(),一般形式是:
int socket(int domain,int type,int protocol);
参数说明:
- domain:指定地址族。最常用的套接字域是
AF_UNIX
和AF_INET
。前者用于通过UNIX文件系统实现的本地套接字,后者可用在穿过包括Internet在内的各种TCP/IP网络而进行通信的应用程序中 - type:指定与这个套接字一起使用的通信类型。取值范围为枚举常量
SOCK_STREAM
和SOCK_DGRAM
。- SOCK_STREAM:一个有序的、可靠的、基于连接的双向字节流。对于一个AF_INET的套接字,如果在两个流式套接字的两段间建立TCP连接,默认即为该特性
- SOCK_DGRAM:一个数据图服务,可以用来发送最大长度是一个固定值的消息,但消息是否会被送达或者消息的先后次序是否会在网络传输中被重新安排并并没有保障。对于一个AF_INET域的套接字来说,这种类型的通信是由UDP提供的。
- protocol:负责制定所使用的协议。默认为0,表示使用默认的协议
系统调用返回一个描述符,与文件描述符非常相似。当这个套接字和通信线路另一端的套接字连接好以后,就可进行数据的传输和接收操作。
3.套接字地址
对于一个AF_UNIX套接字来说,它的地址是由一个包含在sys/un.h
头文件中的sockaddr_un
结构体描述的,定义如下:
struct sockaddr_un
{
sa_family_t sun_family; //AF_UNIX
char sun_path[]; //路径
}
成员说明:
- sun_family:定义地址类型(套接字域)的数据项。sun_family_t是由X/Open技术规范定义的,在Linux系统上被声明为一个short类型。
- sun_path:给出的路径长度是有限制的,Linux规定其最长不超过108个字符。
AF_INET域中的套接字地址是由一个定义在netinet/in.h头文件里的sockaddr_in结构确定的。定义如下:
struct sockaddr_in
{
short int sin_family; //AF_INET
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //Internet地址
}
其中,Internet地址是netinet/in.h头文件中定义的另一个结构体。该结构体的定义为:
struct in_addr
{
unsigned long int s_addr;
}
4.套接字的名字
要使Socket()调用创建的套接字能够被其他进程使用,程序就必须给该套接字起个名字。AF_UNIX套接字会关联到一个文件系统的路径上。 AF_INET套接字将关联到一个IP端口号上。 为套接字命名可使用bind()系统调用:
int bind(int socket,const struct sockaddr* address,size_t addresss_len);
其作用是将参数address中给出的地址赋值给与文件描述符socket相关联的未命名套接字。地址结构的长度是通过address_len参数传递的。地址的长度和类型取决于地址族。
bind()调用需要用一个与之对应的地址结构指针指向真正的地址类型。
该调用成功时将返回0,否则返回-1,并将error变量设置为下表所示的值。
AF_UNIX套接字对应的错误代码比上表要多出两个:一个是EACCESS
,表示权限不足,不能创建文件系统中使用的名字;另一个是ENOTDIR/ENAMETOOLONG
,表示路径错误或路径名太长。
5.创建套接字队列
服务器必须创建一个队列来保存到达的请求。创建队列可使用系统调用listen()完成。一般形式为:
int listen(int socket,int backlog);
Linux系统可能会对队列中能够容纳的排队连接的最大个数有限制。在这个最大值的范围内,listen()将把队列长度设置为backlog个连接。在套接字上排队的接入连接个数最多不能超过这个数字,再往后的连接将被拒绝。backlog的常用值是5。
listen()函数成功时会返回0,否则返回-1,它的错误代码包括EBADF
、EINVAL
和ENOTSOCK
,含义同bind()系统调用的错误代码相同。
6.接受连接
服务器上的应用程序创建好命名套接字之后,就可以通过accept()系统调用来等待客户端程序建立对该套接字的连接。accept()的一般形式是:
int accept(int socket,struct sockaddr* address,size_t* address_len);
accpet()系统调用会等到有客户程序试图连接到由socket参数指定的套接字时才返回。该客户就是套接字队列里排在第一位的连接。accept()函数将创建出一个新的套接字来与该客户进行通信,返回的是与之对应的文件描述符。新套接字的类型与服务器监听套接字的类型相同。
套接字必须是被bind()调用命名过的,并且还要有一个由listen()调用分配的连接队列。客户的地址将被放在address指向的sockaddr结构里。
参数address_len给出了客户结构的长度。如果客户地址的长度超过了这个值,就会被截短。在调用accept()之前,必须把address_len设置为预期的地址长度。当这个调用返回时,address_len将被设置为客户的地址结构的实际长度。
如果套接字队列中无排队等候的连接,accpet()将阻塞程序,直到有客户建立连接为止。这个行为可以用O_NONBLOCK标志改变,方法是对这个套接字文件描述符调用fcntl()
函数,代码如下:
int flags=fcntl(socket,F_GETFL,0);
fcntl(socket,F_SETFL,O_NONBLOCK|flags);
如果有排队等候的客户连接,accept()函数将返回一个新的套接字文件描述符,否则它将返回-1。相比于bind()和listen()调用外,还有一个EWOULDBLOCK,如果前面指定了O_NONBLOCK标志,但队列里没有排队的连接,就会出现这个错误。如果进程阻塞在accept()调用时执行.被中断,就会出现EINTR错误。
7.请求连接
当客户想要连接到服务器时,它会尝试在一个未命名套接字和服务器的监听套接字之间建立一个连接。它们用connect()系统调用来完成这一工作。一般形式是:
int connect(int socket,const int sockaddr* address,size_t,address_len);
参数说明:
- socket:待连接的套接字
- address:连接到的服务器套接字
- address_len:服务器套接字长度
套接字必须是通过socket调用获得的一个有效的文件描述符。如果操作成功,返回0,否则返回-1。该函数产生的错误代码如下:
如果连接不能立刻建立,connect()
会阻塞一段不确定的倒计时时间,结束后这次连接即为失败。如果connect()
调用是被一个信号中断的,而这个信号又得到了处理,connect还是会失败,但这次连接尝试是成功的,它会以异步方式继续尝试。
类似于accept()
调用,connect()
的阻塞特性可以用设置该文件描述符的O_NONBLOCK
标志的办法来改变。在这种情况下,如果连接不能立刻建立,connect()
会失败并把errno变量设置为EINPROGRESS
,而连接将以异步方式继续尝试。
可在套接字文件描述符上用一个select()
调用来表明该套接字已经处于写就绪状态。
8.关闭连接
系统调用close()函数可以结束服务器和客户端上的套接字连接。要想关闭套接字,就必须把服务器和客户两头都关掉才行。对服务器来说,应在read()
返回0时进行该操作,但如果套接字时一个面向连接的类型并设置了SOCK_LINGER
选项,close()
调用会在该套接字尚有未传输数据时阻塞。
9.套接字通信
下面的例子演示套接字通信过程。
(1)服务器程序
#include<sys/types.h>
#include<sys/socket.h> //包含套接字函数库
#include<iostream>
#include<netinet/in.h> //包含AF_INET相关结构
#include<arpa/inet.h> //包含AF_INET相关操作的函数
#include<unistd.h>
using std::cout;
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;
cout<<"服务器等待消息\n";
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_address,(socklen_t * __restrict)&client_len);
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
}
(2)客户端程序
#include<sys/types.h>
#include<sys/socket.h> //包含套接字函数库
#include<iostream>
#include<netinet/in.h> //包含AF_INET相关结构
#include<arpa/inet.h> //包含AF_INET相关操作的函数
#include<unistd.h>
using std::cout;
using std::cerr;
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)
{
cerr<<"连接失败\n";
return 1;
}
write(sockfd,&ch,1);
read(sockfd,&ch,1);
cout<<"来自服务器的消息是"<<ch<<"\n";
close(sockfd);
return 0;
}
客户端向本地的9734端口请求连接,如果连接成功发送一个字符A作为消息,然后从服务器传送的消息中读取一个字符将其输出,退出程序。
运行结果如下: