套接字编程需要指定套接字的地址作为形参,不同的协议族有不同的地址结构定义方式,这些地址结构通常以sockaddr_
开头,每一个协议族有一个唯一的后缀,例如以太网,其结构名称为sockaddr_in
。
1.套接字数据结构
通用套接字地址类型如下,可以在不同协议族之间进行强制转换:
struct sockaddr{ //套接字地址结构
sa_family_t sa_family; //协议族
char sa_data[14]; //协议族数据
}
注:sa_family_t 类型为unsigned short
类型,长度为16字节。
typedef unsigned short sa_faily_t;
2.实际使用的套接字数据结构
网络程序设计中所使用的函数中几乎所有的套接字函数都用这个结构作为参数,例如bind()函数:
int bind(int sockfd,//套接字文件描述符
const struct sockaddr *my_addr,//套接字地址结构
socklen_t addrlen);//套接字地址结构的长度
使用struct sockaddr不方便进行设置,以太网中使用struct sockaddr_in进行设置,如下:
struct sockaddr_in{//以太网套接字地址结构
u8 sin_len;//结构struct sockaddr_in长度,16
u8 sin_family;//通常为AF_INET
u16 sin_port;//16位的端口号,网络字节序
struct in_addr sin_addr;//IP地址为32位
char sin_zero[8];//未用
};
结构struct sockaddr_in
的成员变量in_addr
用于表示IP
地址,这个结构定义如下:
struct in_addr{//IP地址结构
u32 s_addr;//32位IP地址,网络字节序
};
3.结构sockaddr和结构sockaddr_in关系
结构struct sockaddr和结构struct sockaddr_in是一个同样大小的结构,对应关系如下:
struct sockaddr_in的成员含义如下:
sin_len: 无符号字符类型,表示结构struct sockaddr _in的长度,为16。
sin_family: 无符号字符类型,通常设置为与socket()函数的domain一致,例如 AF_INET。
sin_port: 无符号short类型,表示端口号,网络字节序。
sin_addr: struct in_addr类型,其成员s_addr为无符号32位数,每8位表示IP地址的一 个段,网络字节序。
sin_zero[8]: char类型,保留。
进行地址结构设置时,通常的方法是利用结构struct sockadd _in
进行设置,然后强制转换为结构struct sockaddr
类型。因为这两个结构大小是完全一 致的,所以进行这样的转换不会有副作用。
套接字参数中有部分参数是需要用户传入的,这些参数用来与Linux内核进行通信,例如指向地址结构的指针。通常是采用内存复制的方法进行。
struct sockaddr *my_ addr
和my_addr
指向参数的长度。1.向内核传入数据的交换过程
向内核传入数据的函数有send()、bind()等,从内核得到数据的函数有accept()、recv()等。
传入的过程如下图所示,bind()函数向内核中传入的参数有套接字地址结构和结构的长度两个与地址结构有关的参数。
参数addlen表示地址结构的长度,参数my_addr是指向地址结构的指针。
调用函数bind()的时候,地址结构通过内存复制的方式将其中的内容复制到内核,地址结构的长度通过传值的方式传入内核,内核按照用户传入的地址结构长度来复制套接字地址结构的内容。
2.内核传出数据的交换过程
从内核向用户空间传递参数的过程则相反,传出的过程如下图所示。
通过地址结构的长度和套接字地址结构指针来进行地址结构参数的传出操作。
通常是两个参数完成传出作的功能,一个是表示地址结构长度的参数,另一个是表示套接字地址结构地址的指针。
传出与传入中的参数有所不同,表示地址结构长度的参数在传入过程中是传值,而在传出过程中是通过传址完成的。
内核按照用户传入的地址结构长度进行套接字地址结构数据的复制,将内核中的地址结构数据复制到用户传入的地址结构指针中。
TCP网络编程有两种模式:
服务器模式:服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;
客户端模式:客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。
1.服务器端的程序设计模式
流程主要分为:
下图为TCP连接的服务器模式的程序设计:
套接字初始化根据用户对套接字的需求来确定套接字的选项。这个过程中的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
套接字与端口的绑定将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址,以及协议类型等参数按照绑定值进行操作。
服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。
在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。
在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。
当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。
2.客户端的程序设计模式
主要分为:
如下图所示为TCP客户端模式:
不同之处是客户端在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端。
3.客户端与服务器的交互过程
客户端与服务器在连接、读写数据、关闭过程中有交互过程:
客户端的连接过程,对服务器端是接收过程,在这个过程中客户端与服务器进行三次握手,建立TCP连接。建立TCP连接之后,客户端与服务器之间可以进行数据的交互。
客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器的读数据过程。
在服务器和客户端之间的数据交互完毕之后,关闭套接字连接。
socket()函数原型如下:调用成功返回表示这个套接字的文件描述符,失败返回-1。
#incluede<sys/types.h>
#include
int socket(int domain,int type,int protocol);//协议族domain;协议类型type;协议编号protocol
domain
用于设置网络通信的域,函数根据此参数选择通信协议的族。通信协议族在文件sys/socket.h
中定义,包含下表所有值,以太网中应该使用PF_INET
这个域,现有代码使用AF_INET
这个值,在头文件中两个值是相同的。
type
参数type
用于设置套接字通信的类型,如下表所示type格式定义及含义。主要有SOCK_STREAM(流式套接字)
、SOCK_DGRAM(数据包套接字)
等。
不是所有协议族都实现了这些协议,例:AF_INET
协议族就没有实现SOCK_SEQPACKET
协议类型。
protocol
用于指定某个协议类型,即type类型中某个类型。通常某个协议中只有一种特定类型,这样protocol参数仅能设置为0。但有些协议有很多种特定的类型,就需要设置这个参数来选择特定的类型。
SOCK_STREAM
的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行,连接成功使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一 段时间内仍然没有接收完毕,可以认为这个连接已经死掉。
SOCK_DGRAM
和SOCK_RAW
这两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接收数据,recvfrom()接收来自指定IP地址的发送方的数据。
SOCK _PACKET
是一种专用的数据包,它直接从设备驱动接收数据。
函数socket()执行过程可能会出现错误,可以通过ermo
获得,具体值和含义在如下表。
通常情况下造成函数socket()失败的原因是输入的参数错误造成的,例如某个协议不存在等,这时需要详细检查函数的输入参数。
由于函数的调用不一 定成功,在进行程序设计的时候,一定要检查返回值。
使用sockt()函数需要设置上述3个参数,如将socket()函数的第一个参数domain设置为AF_INET,第二个参数设置为SOCK_STREAM,第三个参数设置为0,建立一个流式套接字。
int sock = socket(AF_INET,SOCK_STREAM,0);
2.应用层函数socket()和内核函数之间关系
用户设置套接字参数后,函数能够起作用,需要与内核空间的相系统交互,应用层的socket()函数是和内核层的系统调用相对应的,如下图所示:
函数sock=socket(AF_INET,SOCK_STREAM,0)
,此函数调用系统函数sys_socket(AF_INET,SOCK_STREAM,0),(在文件net/socket.c中)
。系统调用函数分为两部分,一部分生成内核socket结构(z注意于应用层的socket()函数是不同的),另一部分与文件描述符绑定,将绑定的文件描述符值传递给应用层。内核sock结构如下(在文件linux/net.h
):
struct socket{
socket state;//socket状态(例如SS_CONNECTED等)
unsigned long flags;//socket标志(SOCK_ASYNC_NOSPACE等)
const struct proto_ops *ops;//协议特定的socket操作
struct fasync_struct *fasync_list;//异步唤醒列表
struct file *file;//文件指针
struct sock *sk;//内部网络协议结构
wait_queue_head_t wait;//多用户时的等待队列
short type;//socket类型(SOCK_STREAM等);
};
内核函数sock_create()
根据用户的domain
指定协议族,创建一个内核socket结构绑定到当前的进程上,其中type
与用户空间用户的设置值是相同的。
sock_map_fd()
函数将socket结构与文件描述符列表中的某个文件描述符绑定,之后的操作可以查找文件描述符列表来对内核socket结构。
建立套接字文件描述符成功后,需对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作。
1.bind()函数介绍
bind()函数将长度为addlen
的struct sockadd
类型的参数my_addr
与sockfd
绑定在一起,将socked绑定到某个端口上,如果使用connect()函数则没有绑定的必要。绑定的函数原型如下:
#include
#include
int bind(int sockfd,const struct socket *my_addr,socklen_t addrlen);
sockfd:函数创建的文件描述符。
my_addr:指向一个结构为sockaddr参数的指针,sockaddr包含地址、端口、IP地址信息。绑定时需将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置后才能进行绑定,绑定后才能将套接字文件描述符与地址等结合在一起。
addrlen:表示my_addr结构的长度,可以设置成sizeof(struct sockaddr)。一般使用AF_INET设置套接字的类型和其他对应的结构,但不同类型的套接字有不同的地址描述符,强制指定地址长度,可能造成不可预料的后果。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败,erron的错误值如下:
下面代码初始化一个AF_UNIX族中的SOCK_STREAM类型的套接字。先使用结构struct sockaddr_un初始化my_addr,然后绑定,结构stuct sockaddr_un定义如下:
struct sockaddr_un{
sa_family_t sun_family;//协议族,应该设置为AF_UNIX
char sun_path[UNIX_PATH_MAX];//路径名。UNIX_PATH_MAX的值为108
};
2.bind()函数的例子
使用bind()函数进行程序设计的一个实例,先建立一个UNIX族的流类型套接字,然后将套接字地址和套接字文件描述符进行绑定:
#define MY_SOCK_PATH "/somepath"
int main(int argc,char *argv[])
{
int sfd;
struct sockaddr_un addr;//AF_UNIX对应的结构
sfd = socket(AF_UNIX,SOCK_STREAM,0);//初始化一个AF_UNIX族的流类型socket;将协议族参数设置为AF_UNIX建立为UNIX族套接字,使用函数socket()进行建立。
if(sfd == -1)//检查是否正常初始化socket
{
perror("socket");
exit(EXIT_FAILURE);
}
memset(&addr,0,sizeof(struct sockaddr_un));//将变量addr置0;初始化地址结构,将UNIX地址结构设置为0,这是进行程序设计时常用的初始化方式
addr.sun_family = AF_UBIX;//协议族为AF_UNIX
strncpy(addr.sun_path,MY_SOCK_PATH,//复制路径到地址结构
sizeof(addr.sun_path - 1);
if(bind(sfd, (struct sockaddr *) &addr,//绑定
sizeof(struct sockaddr_un))==-1)//绑定并判断是否成功
{
perror("bind");
exit(EXIT_FAILURE);
}
...//数据接收发送及处理过程
close(sfd);//关闭套接字文件描述符
}
注:Linux 的GCC编译器有一个特点,一个结构的最后一个成员为数组时,这个结构可以通过最后一个成员进行扩展,可以在程序运行时笫一次调用此变量的时候动态生成结构的大小。例如上面的代码,并不会因为 struct sockaddr_un
比struct sockaddr
大而溢出。
另一个使用结构struct sockaddr_in绑定一个AF_INET族到流协议,先将结构struct sockaddr_in的sin_family设置为AF_INET,然后设置端口,接着设置一个IP地址,最后进行绑定:
#define MYPORT 3490 //端口地址
int main(int arg,char *argv[])
{
int sockfd;//套接字文件描述符变量
struct sockaddr_in my_addr;//以太网套接字地址结构
sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化socket
if(sockfd == -1){ //检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
my_addr.sin_family = AF_INET;//地址结构的协议族
my_addr.sin_port = htons(MYPORT);//地址结构的端口地址,网络字节序,使用htins()进行字节序转换。
my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");//IP,将字符串的IP地址转化为网络字节序
bzero(&(my_addr.sin_zero),8);//将my_addr.sin_zero置为0
if(bind(sockfd,(struct sockaddr *)&myadd,
sizeof(struct sockaddr)) == -1){ //判断是否绑定成功
perror("bind");
exit(EXIT FAILURE);
}
...//接收和发送数据,进行数据处理
close(sockfd);//关闭套接字文件描述符
}
3.应用层bind()函数和内核函数之间关系
bind()是应用层函数,要使用函数生效,就要将相关的参数传递给内核并进行处理,应用层的bind()函数与内核之间的函数过程如下所示,图中是一个AF_INET族函数进行绑定的调用过程:
应用层的函数bind(sockfd,(struct sockaddr*)&my_ addr, sizeof(struct sockaddr))
调用系统函数过程sys_bind(sockf d,(struct sockaddr*)&my_ addr, sizeof(struct sockaddr))
。
sys_bind()
函数首先调用函数sockfd_lookup _light()
来获得文件描述符sockfd对应的内核struct sock结构变量,然后调用函数move_addr_to_kemel()
将应用层的参数my_addr复制进内核,放到address变量中。
内核的sock结构是在调用socket()函数时根据协议生成的,它绑定了不同协议族的bind()函数的实现方法,在AF_INET族中的实现函数为inet_bind()
, 即会调用AF_INET族的bind()函数进行绑定处理。
函数listen()用来初始化服务器可连接队列,多个客户端连接请求同时到来时,服务器不会同时处理,而是将不能处理的客户端连接请求放到等待队列中,队列长度由listen()函数定义。
1.listen()函数介绍
listen()函数原型如下,其中的backlong表示等待队列的长度:
#include
int listen(int sockfd,int backlog);
运行成功时,返回0,失败返回-1,并且设置erron值,错误代码含义如下:
ECONNREFUSED
错误。SOCK_STR EAM
或者SOCK _SEQPACKET
的协议有效,例如,如果对一 个 SOCK_DGRAM
的协议使用函数 listen(), 将会出现错误 errno应该为值EOPNOTSUPP
, 表示此 socket不支持函数 listen()操作。大多数系统的设置为 20, 可以将其设置修改为 5或者10, 根据系统可承受负载或者应用程序的需求来确定。2.listen()函数的例子
在成功进行socket()函数初始化和bind()函数1端口之后,设置listen()函数队列的长度为5。
#define MYPORT 3490 // 端口地址
int main(int argc,char *argv[])
{
int sockfd;//套接字文件描述符变量
struct sockaddr_in my_addr;
sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化socket
if(sockfd == -1){//检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
my_addr.sin_family = AF_INET;//地址结构的协议族
my_addr.sin_port = htons(MYPORT);//地址结构的端口地址,网络字节序
my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");//IP,将字符串的IP地址转化为网络字节序
bzero(&(my_addr.sin_zero),8);//将my_addr.sin_zero置0
if(bind(sockfd,(struct sockaddr *)&my_addr,
sizeof(struct sockaddr)) == -1){//判断是否绑定成功
perror("bind");//打印错误信息
exit(EXIT_FAILURE);//退出程序
}
if(listen(sockfd,5) == -1){//进行侦听队列长度绑定、判断是否listen成功
perror("listen");
exit(EXIT_FAILURE);
}
...//接收数据、发送数据和数据的处理过程
close(sockfd);//关闭套接字
}
3.应用层listen()函数和内核函数之间关系
应用层listen()函数和内核层listen()函数的关系如下图所示,应用层的listen()
函数对应于系统调用sys _listen()
函数。
sys _listen()
函数首先调用sockfd_ lookup _light()
函数获得sockfd对应的内核结构struct socket
, 查看用户的backlog设置值是否过大,如果过大则设置为系统默认最大设置。
然后调用抽象的listen()函数,这里指的是 AF_INET
的listen()函数和inet_listen()函数。
inet_listen()函数首先判断是否合法的协议族和协议类型,再更新socket的状态值为TCP_LISTEN
,然后为客户端的等待队列申请空间并设定侦听端口。
客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,直到使用服务器处理接受请求。
函数accept()成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据发送。
1.accept()函数介绍
函数原型:
#include
#include
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
accept()函数可以得到成功连接客户端的 IP 地址、端口和协议族等信息,这个信息是通过参数 addr
获得的。
当accept()函数返回的时候,会将客户端的信息存储在参数 addr中,参数 addrlen
表示第2个参数(addr)所指内容的长度,可以使用 sizeof(structsockaddr _in)
来获得。
需要注意在 accept中addrlen 参数是一 个指针
而不是结构,accept()函数将这个指针传给 TCP/IP
协议栈。
accpet()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()函数返回的新套接字文件描述符来进行的,而不是通过建立套接字时的文件描述符,这是在程序设计时候需要注意的地方。
如果accept()函数发生错误,accept()函数会返回-1。通过errno可以得到错误值,含义如下:
2.accept()函数的例子
首先建立一个流式套接字然后对套接字进行地址绑定,当绑定成功后,初始化侦听队列的长度,然后等待客户端的连接请求。
int main(int argc,char *argv[])
{
int sockfd,clinet_fd;//sockfd为侦听的socket,clinet_fd为连接方的socket值
struct sockaddr_in my_addr;//本地地址信息
struct sockaddr_in client_addr;//客户端连接的地址信息
int addr_length;//int类型变量,用于保存网络地址长度量
socket = socket(AF_INET,SOCK_STREAM,0);//初始化一个IPv4族的流式连接
if(sockfd == -1){//检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
my_addr.sin_family = AF_INET;//协议族为IPv4,主机字节序
my_addr.sin_port = htons(MYPORT);//端口,短整型,网络字节序
my_addr.sin_addr.s_addr = INADDR_ANY;//自动IP地址获得
bzero(&(my_addr.sin_zero),8);//将sin_zero置0
if(bind(sockfd,(struct sockaddr *)&my_addr,//绑定端口地址
sizeof(struct sockaddr)) == -1){//判断是否绑定成功
perror("bind");
exit(EXIT_FAILURE);
}
if(listen(sockfd,BACKLOG) == -1){//设置侦听队列长度BACKLIG=10并判断是否listen成功
perror("listen");
exit(EXIT_FAILURE);
}
addr_lenth = sizeof(struct sockaddr_in);//地址长度
client_fd = accept(sockfd,&client_addr,&addr_length);//等待客户端连接,地址在client_addr中
if(client_fd == -1){//accept出错
perr("accept");
exit(EXIT_FAILURE);
}
...//处理客户端连接过程
close(client_fd);//关闭客户端连接
...//其他过程
close(sockfd);//关闭服务器端连接
}
3.应用层accept()函数和内核函数之间关系
accept()
函数对应内核层的sys_accept()
函数系统调用函数。
函数sys_accept()
调用函数sockfd_lookup_light()
查找到文件描述符对应的内核socket结构后,然后会申请一块内存用于保存连接成功的客户端的状态。
socket结构的一些参数,如类型type、操作方式ops等会继承服务器原来的值,假如原来服务器的类型为AF_INET
,则其操作模式仍然是af_inet.c
文件中的各个函数。
然后会查找文件描述符表,获得一个新结构对应的文件描述符。
accept()函数的实际调用根据协议族的不同而不同,即函数指针sock->ops->accept
要由socket()函数初始化时的协议族而确定。当为AF_INET
时,此函数指针对应于af_inet.c
文件中的inet_accept()
函数。
当客户端连接成功后,内核准备连接的客户端的相关信息,包含客户端的IP地址、客户端的端口等信息,协议族的值继承原服务器的值。
在成功获得信息之后会调用move_addr_to_user()
函数将信息复制到应用层空间,具体的地址由用户传入的参数来确定。
客户端建立套接字后,不需要进行地址绑定可以直接连接服务器。连接服务器的函数为connect(),此函数连接指定参数的服务器,例如IP地址、端口等。
1.connect()函数介绍
#include
#include
int connect(int sockfd,struct sockaddr*,int addrlen);
sizeof(struct sockaddr)
获得,与bind()函数不同。connect()函数返回值成功时为0,错误返回-1,可查看errno获得错误原因,如:
2.connect()函数例子
先建立一个套接字文件描述符,成功建立描述符后,将需要连接的服务器IP地址和端口填充到一个地址结构中,connect()函数连接到地址结构所指定的服务器上。
#define DEST_IP "132.241.5.10" // 服务器的IP地址
#define DEST_PORT 23 //服务器端口
int main(int argc.char *argv[])
{
int ret=0;
int socket;//sockfd为连接的socket
struct sockaddr_in server;//服务器地址信息
sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化一个IPv4族的流式连接
if(socket == -1){
perror("socket");
exit(EXIT_FAILURE);
}
server.sin_family = AF_INET;//协议族为IPv4,主机字节序
server.sin_port = htons(DEST_PORT);//端口、短整型
server.sin_addr.s_addr.s_addr = htonl(DEST_IP);//服务器的IP地址
bzero(&(server.sin_zero),8);//保留字段置 0
ret=connect(sockfd,(struct sockaddr *)&server,sizeof(struct sockaddr));//连接服务器
...//接收或者发送数据
close(sockfd);
}
3.应用层connect()函数和内核之间的关系
connect()函数主要进行不同的协议映射的时候要根据协议的类类型进行选择,例如流式的回调函数为inet_stream_connetc(),数据的回调函数为inet_dgram_connect()。如下图:
服务器端接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式与普通文件操作形式一样,内核根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相应的内核函数。
例:向套接字文件描述符中写入数据的例子,将缓冲区的data的数据全部写入套接字文件描述符s中,返回成功写入数据长度。
int size;
char data[1024];
size = write(s,data,1024);
//字节流套接字上的read和write函数所表现的行为不同于通常的文件IO。
//字节流套接字上调用read和write输入或输出的字节数可能比请求的数量少,
//因为内核中用于套接字的缓冲区是有限制的,需要调用者多次调用read或write函数。
//像描述符filedes中写入nbytes个字节,从buff位置开始写
ssize_t writen(int filedes,const void *buff,size_t nbytes);
ssize_t writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return(-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return(n);
}
void Writen(int fd, void *ptr, size_t nbytes)
{
if (writen(fd, ptr, nbytes) != nbytes)
err_sys("writen error");
}
read()函数从套接字描述符中读取数据。(注:读取之前必须建立套接字并连接)。例:从套接字描述符s中读取1024个字节,放入缓冲区data中,size变量的值为成功读取的数据大小。
int size;
char data[1024];
size = read(s,data,1024);
//字节流套接字上的read和write函数所表现的行为不同于通常的文件IO。
//字节流套接字上调用read和write输入或输出的字节数可能比请求的数量少,因为内核中用于套接字的缓冲区是有限制的,
//需要调用者多次调用read或write函数。
//解决问题:
//从描述符filedes中读取nbyes个字节,存入buff指针的位置。
ssize_t readn(int filedes,void *buff,size_t nbytes);
、
//例:
ssize_t readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return(-1);
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return(n - nleft);
}
ssize_t
Readn(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
if ( (n = readn(fd, ptr, nbytes)) < 0)
err_sys("readn error");
return(n);
}
//从描述符filedes中读一行文本,长度不超过maxlen,一次读1个字节。存放在buff位置
ssize_t readline(int filedes,void *buff,size_t maxlen);
//例
ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
ssize_t n;
if ( (n = readline(fd, ptr, maxlen)) < 0)
err_sys("readline error");
return(n);
}
ssize_t readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break; /* newline is stored, like fgets() */
} else if (rc == 0) {
*ptr = 0;
return(n - 1); /* EOF, n - 1 bytes were read */
} else
return(-1); /* error, errno set by read() */
}
*ptr = 0; /* null terminate like fgets() */
return(n);
}
#include "unp.h"
static int read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];
//每次最多读MAXLINE个字符,然后每次返回一个字符
static ssize_t my_read(int fd, char *ptr)
{
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return(-1);
} else if (read_cnt == 0)
return(0);
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return(1);
}
//readline较快版本
ssize_t readline(int fd, void *vptr, size_t maxlen)//my_read取代read
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break; /* newline is stored, like fgets() */
} else if (rc == 0) {
*ptr = 0;
return(n - 1); /* EOF, n - 1 bytes were read */
} else
return(-1); /* error, errno set by read() */
}
*ptr = 0; /* null terminate like fgets() */
return(n);
}
//展露内部缓冲区的状态,便于调用者查看当前文本之后是否收到新数据
ssize_t readlinebuf(void **vptrptr)
{
if (read_cnt)
*vptrptr = read_ptr;
return(read_cnt);
}
/* end readline */
ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
ssize_t n;
if ( (n = readline(fd, ptr, maxlen)) < 0)
err_sys("readline error");
return(n);
}
close()用于关闭已经打开的socket连接,内核会释放相关资源,关闭之后不能使用这个套接字文件描述符进行读写操作。
函数shutdown()可以使用更多方式来关闭连接,允许单方向切断通信或者切断双方的通信。
#include
int shutdown(int s,int how);//s指切断通信的套接字文件描述符,how表示切断方式
此函数用于关闭双向连接的一部分,具体的关闭行为方式通过参数how设置实现。可以如下:
SHUT_RD
:值为0,表示切断读,之后不能使用此文件描述符进行读操作。SHUT_WR
,值为1,表示切断写,之后不能使用此文件描述符进行写操作。SHUT_RDWR
:值为2,表示切断读写,之后不能使用此文件描述符进行读写操作与close()函数功能相同。函数shutdown()调用成功返回0,失败返回-1.通过errno获得错误信息:
#include
//调用一次,返回两次
//一次在父进程中返回子进程PID,一次在子进程中返回0。
//派生进程
pid_t fork(void);
//如果fork进程是unix程序员唯一可以创建进程的手段,那么linux性能会非常差,而且只能fork出同样的进程。
//exec函数就是解决这个问题,它把一个新的程序装载进进程的内存空间,来改变调用进程的执行代码,
//相当于产生一个新的进程,故通常exec使用方式是先fork一个子进程,然后exec执行进程。
//exec并没有产生一个新的进程,而是执行一个可执行文件,让该进程取代原有的进程
#include
extern char **environ; //系统环境变量
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
//区别:
//fork会产生一个完全相同的进程,是两个进程;exec则本进程会加载可执行文件成为一个新的进程,只有一个进程。
// 这两个函数的最后一个参数都是值-结果参数,(这两个函数都得装填由localaddr和peeraddr指针所指的套接字地址结构。)
#include
//返回套接字关联的本地协议地址
int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen);
//返回套接字关联的外地协议地址
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen);
#include "unp.h"
int sockfd_to_family(int sockfd)
{
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if (getsockname(sockfd, (SA *) &ss, &len) < 0)
return(-1);
return(ss.ss_family);
}
/* end sockfd_to_family */
int
Sockfd_to_family(int sockfd)
{
int rc;
if ( (rc = sockfd_to_family(sockfd)) < 0)
err_sys("sockfd_to_family error");
return(rc);
}
程序的代码如下,程序按照网络流程建立套接字、初始化绑定网络地址、将套接字与网络地址绑定、设置侦听队列长度、接收客户端连接、收发数据、关闭套接字进行编写。
#include
#include
#include
#include
#include
#include
#include
#define PORT 8888//侦听端口地址
#define BACKLOG 2//侦听队列长度
int main(int argc,char *argv[])
{
int ss,sc;//ss:服务器的socket描述符,sc:客户端的socket描述符
struct sockaddr_in server_addr;//服务器地址结构
struct sockaddr_in client_addr;//客户端地址结构
int err;//返回值
pid_t pid;//分叉进行ID
ss = socket(AF_INET,SOCK_STREAM,0);//建立流式套接字
if(ss<0){
printf("socket error\n");
return -1;
}
//设置服务器地址
bzero(&server_addr,sizeof(server_addr));//清零
server_addr.sin_family = AF_INET;//协议族
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址
server_addr.sin_port = htons(PORT);//服务器端口
//绑定地址结构到套接字描述符
err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
//设置侦听
if(err<0){
printf("bind error\n");
return -1;
}
err = listen(ss,BACKLOG);
if(err<0){
printf("listen error\n");
return -1;
}
for(;;){
socklen_t addrlen = sizeof(struct sockaddr);
sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);//接收客户端连接
if(sc<0){
continue;
}
pid = fork();//分叉进程
if(pid ==0)//子进程
{
close(ss);//在子进程关闭服务器侦听;通常调用
process_conn_server(s);
}else{
close(sc);//在父进程中关闭客户端的连接
}
}
}
一个进中的套接字文件描述符的关闭,不会造成套接字的真正关闭,因为任然有一个进程在使用这些套接字描述符,只有所有的进程都关闭了这些描述符,Linux才释放,子进程中,处理通过调用函数process_conn_server()来完成。
建立一个流式套接字后,将服务器的地址和端口绑定到套接字描述符上,然后连接服务器,进程处理,最后关闭连接。
#include
#include
#include
#include
#include
#include
#include
#define PORT 8888//侦听端口地址
extern void process_conn_client(int s);
int main(int argc,char *argv[])
{
int s;//s:socket描述符
struct sockaddr_in server_addr;//服务器地址结构
s=socket(AF_INET,SOCK_STREAM,0);//建立流式套接字
if(s<0){
printf("socket error\n");
return -1;
}
//设置服务器地址
bzero(&server_addr,sizeof(server_addr));//清零
server_addr.sin_family = AF_INET;//协议族
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址
server_addr.sin_port = htons(PORT);//服务器端口
//将用户输入的字符串类型的IP地址转换整型
inet_pton(AF_INET,argv[1],&server_addr.sin_addr);
//连接服务器
connect(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr));
process_conn_client(s);//客户端处理过程
close(s);//关闭连接
return 0;
}
#include
#include
#include
#include
#include
#include
#include
//先读取从客户端发来的数据,然后将接收到的数据发送给客户端
void process_conn_server(int s)//服务器对客户端的处理
{
ssize_t size = 0;
char buffer[1024];//数据的缓冲区
for(;;){//循环处理
size = read(s,buffer,1024);//从套接字中读取数据放到缓冲区buffer中
if(size ==0 ){//没有数据
return;
}
//构建响应式字符,为接收到客户端字节的数量
sprintf(buffer,"%ld bytes altogether\n",size);
write(s,buffer,strlen(buffer)+1);//发给客户端
}
}
//客户端从标准输入读取数据到缓冲区buffer中,发送服务器端,然后从服务器端读取服务器的响应,将数据发送到标准输出。
void process_conn_client(int s)
{
ssize_t size = 0;
char buffer[1024];//数据的缓冲区
for(;;){//循环处理
size = read(0,buffer,1024);
if(size>0)//读到数据
{
write(s,buffer,size);//发送给客户端
size = read(s,buffer,1024);//从服务器读取数据
write(1,buffer,size);//写到标准输出
}
}
}
注:使用read()和write()函数时,文件描述符0表示标准输入,1表示标准输出,可以直接对这些文件描述符进行操作,例如读和写。
服务器:tcp_server.c,客户端:tcp_client.c,服务器/客户端 读取和显示字符串:tcp_process.c,建立Makefile文件:
all:client server
client:tcp_process.o tcp_client.o
gcc -o client tcp_process.o tcp_client.o
server:tcp_process.o tcp_server.o
gcc -o server tcp_process.o tcp_server.o
tcp_process.o:
gcc -c tcp_process.c -o tcp_process.o
clean:
rm -f client server *.o
运行服务器可执行程序server,侦听8888端口,等待客户端连接请求。
在另一个窗口运行客户端,输入hello和nihao字符串,服务器将客户端发送的数据进行计算并返回客户端:
使用netstat查看网络连接情况,8888服务器端口,55143的端口,服务器和客户端通过这两个端口建立连接。
在Linux操作系统中出现某些状况时,系统会向相关的进程发送信号。
信号的处理方式是系统会先调用进程中注册的处理函数,然后调用系统默认的响应方式,包括终止进程。
因此在系统结束进程前,注册信号处理函数进行一些处理是一个完善程序的必备条件。
信号是发生某件事情时的 一个通知,有时候也将称其为软中断。信号将事件发送给相关的进程,相关进程可以对信号进行捕捉并处理。信号的捕捉由系统自动完成,信号处理函数的注册通过函数signal()
完成。函数signal()的原型为:
#include
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
向信号signum注册一个void(*sighandler_t)(int)
类型的函数,函数的句柄handler。
进程捕捉到注册的信号时,会调用响应式函数句柄handler。信号处理函数在处理系统默认的函数之前会被调用。
如果正在写入套接字的时候,当读取端已经关闭时,可以得到一个 SIGPIPE
信号 。
信号SIGPIPE
会终止当前进程,因为信号系统在调用系统默认处理方式之前会先调用用户注册的函数,所以可以通过注册 SIGPIPE 信号的处理函数来获取这个信号,并进行相应的处理。
例如,当服务器端已经关闭,而客户端试图向套接字写入数据的时候会产生一个SIGPIPE 信号,此时将造成程序的非正常退出。可以使用signal()
函数注册一个处理函数,释放资源,进行一 些善后工作。下面的例子将处理函数sig_pipe()
挂接到信号SIGPIPE 上。
void sig_pipe(int sign)
{
printf("Catch a SIGPIPE signal\n");
/*释放资源*/
}
signal(SIGPIPE,sig_pipe);
将此代码加到上面客户端程序中,进行信号测试:在客户端连接后,退出服务器程序。当标准输入有数据的时候,客户端会通过套接字描述符发送数据到服务器端,而服务器已经关闭,因此客户端会收到一 个SIGPIPE 信号。其输出如下:
Catch a SIGPIPE signal
信号SIGINT
通常由Ctrl+C
终止造成的,与Ctrl+C一致,kill
命令默认发送SIGINT信号,用于终止进程运行向当前活动的进程发送这个信号:
void sig_int(int sign)
{
printf("Catch a SIGINT signal\n");
/*释放资源*/
}
signal(SIGINT,sig_pipe);
一个子进程子在父进程还没有调用wait()或waitpid()的情况下退出,这个进程就是僵尸进程。
两种解决方法:
1.捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数
2.两次frok。
设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。
信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间、内存使用量等等)。
如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。
继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
SIGCHLD
信号的信号处理函数,在函数体中我们调用wait.#include "unp.h"
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
printf("child %d terminated\n", pid);
}
return;
}
僵尸进程中子进程结束后,需要由父进程回收子进程,父进程不能先于子进程结束的, 当子进程结束后会通知父进程(子进程会给父进程发送SIGCHILD信号),然后父进程接收到通知就会处理子进程了,这时就需要用到wait函数了。
//都返回两个值:1.已终止进程的进程ID号;2.通过status指针返回的子进程终止状态(整数)
//三个宏检查终止状态:
1.WIFEXITED(status) //返回非0表示进程正常结束
WEXITSTATUS(status) //如上面宏函数为真,则使用此宏函数获取进程退出状态 (exit的参数)
2. WIFSIGNALED(status) //返回非0表示进程异常终止
WTERMSIG(status) //如上面宏函数为真,使用此宏函数取得使进程终止的那个信号的编号
3. WIFSTOPPED(status) //返回非0表示进程处于暂停状态
WSTOPSIG(status) //如上面宏函数为真,使用此宏函数取得使进程暂停执行的那个信号的编号
WIFCONTINUED(status) //如上面宏函数为真,使用此宏函数让进程暂停后已经恢复运行
//
#include
//wait()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status 返回, 而子进程的进程识别码也会一起返回. 如果不在意结束状态值, 则参数 status 可以设成NULL. 。
pid_t wait(int *status);//status:用于保存子进程的退出状态等信息(传出参数)。
//父进程在调用wait函数回收子进程时,wait函数将会执行以下几个动作:
//阻塞等待子进程退出;回收子进程残留资源;获取子进程结束状态(退出原因)。
#include
#include
#include
#include
int main(void) {
pid_t pid, wpid;
int status;
pid = fork();
if (pid == 0) {
sleep(300);
printf("child, pid = %d\n", getpid());
return 19;
} else if (pid > 0) {
printf("parent, pid = %d\n", getpid());
//获取子进程的状态
//父进程一直在阻塞等待子进程结束
wpid = wait(&status);
printf("wpid ---- = %d\n", wpid);
if (WIFEXITED(status)) {
//获取进程退出状态
printf("exit with %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
//获取使进程异常终止的信号编号
printf("killed by %d\n", WTERMSIG(status));
}
}
}
// waitpid函数功能和wait一样,都是用来获取子进程状态或改变,但是waitpid函数更加强大,但可指定pid进程回收,可以不阻塞。
pid_t waitpid(pid_t pid,int *status,int options);
/*参数pid:指定回收子进程pid
pid > 0,回收指定pid的子进程
pid = -1,回收任意子进程(相当于wait)
pid = 0,回收和当前调用waitpid一个进程组内的所有子进程
pid < -1,回收指定进程组内为|pid|的所有子进程
参数status:传出参数,用于保存清理子进程的状态(如果不关心子进程的退出状态可传NULL)
参数options:设置回收状态阻塞或非阻塞
WUNTRACED:可获取子进程暂停状态,也就是可获取stopped状态
WCONTINUED(linux2.6.10内核):可获取子进程恢复执行的状态,也就是可获取continued状态
WNOHANG:设置非阻塞,如果参数pid指定的子进程运行正常未发生状态改变,则立即返回0,如果调用进程没有与pid匹配的子进程,waitpid则出错,设置errno为ECHILD。
参数options是一个位掩码,可以使用|运算符操作组合以上几个标志,如果options = NULL(也就是这三个开关都不打开),调用waitpid函数会以阻塞方式回收子进程,这点需要注意。
另外,对于waitpid的参数status值也可以使用WIFEXITED,WIFSIGNALED,WIFSTOPPED等宏函数来进一步判断进程终止的具体原因。*/
//注:使用waitpid函数的时候需要注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环
#include
#include
#include
#include
/*
主要思路:熟悉waitpid函数原型,参数作用和返回值
*/
int main(void) {
pid_t pid, wpid;
int flg = 0;
pid = fork();
//fork进程失败
if(pid == -1){
perror("fork error");
exit(1);
//子进程
} else if(pid == 0){
printf("I'm process child, pid = %d\n", getpid());
sleep(5);
exit(4);
//父进程
} else {
do {
//WNOHANG非阻塞回收指定子进程
wpid = waitpid(pid, NULL, WNOHANG);
//wpid = wait(NULL);
printf("---wpid = %d--------%d\n", wpid, flg++);
//如果wpid == 0说明参数3为WNOHANG非阻塞回收,且子进程正在运行中
if(wpid == 0){
printf("NO child exited\n");
sleep(1);
}
//每次循环前,判断子进程是否可回收
} while (wpid == 0);
//如果为真,可回收指定子进程
if(wpid == pid){
printf("I'm parent, I catched child process, pid = %d\n", wpid);
//回收的任意子进程
} else {
printf("other...\n");
}
}
return 0;
}
//与waitpid函数类似,waitid函数是用于获取一个子进程更加详细的状态或改变。waitid是源于System V下的系统调用,现在已加入到2.6.9 linux内核中。
#include
int waitid( idtype_t idtype, id_t id, siginfo_t *infop, int options );
/*参数idtype和id指定需要获取那些子进程的状态:
1.如果idtype = P_ALL,则获取任意子进程状态,并忽略参数id
2.如果idtype = P_PID,获取参数id指定的子进程状态
3.如果idtype = P_PGID,则获取进程组为id的所有子进程
waitid函数和waitpid函数最大的区别在于,waitid可以通过参数options更加精准详细的获取子进程的状态或改变:
WEXITED:获取正常终止的子进程状态
WSTOPPED:获取因信号而暂停的子进程
WCONTINUED:获取由SIGCONT信号恢复执行的子进程
WNOHANG:同waitpid函数中的选项意义相同
WNOWAIT:从“可等待状态”的子进程处返回,后面的wait依然可以获取子进程状态
*/
// 调用waitid函数返回0会将子进程相关信息保存到参数infop中,infop是一个传出参数,它的数据类型是siginfo_t结构体,以下是siginfo结构体中的比较重要的成员信息:
siginfo_t {
int si_signo; //表示信号,如SIGCHILD信号
int si_code; //一般有这几个值,CLD_EXITED表示子进程是调用_exit终止的,CLD_KILLED表示子进程因某个信号杀死的,CLD_STOPPED表示子进程因某个信号而终止,CLD_CONTINUED表示子进程收到SIGCONT信号恢复执行。
pid_t si_pid; //发送信号的进程id
uid_t si_uid; //发送信号的进程实际用户
int si_status; //进程退出的原因,比如正常退出对应的值,异常退出对应的信号。子进程具体的退出原因可以通过si_code来进一步判断。
. . . . . .
}
//若参数options指定了WNOHANG,waitid函数会在以下几种情况下返回0:子进程的状态已经改变,或者子进程的状态没有改变。为了确保区分这两种情况,最好是在调用waitid函数之前把siginfo_t结构体清空:
siginfo_t info;
memset(&info , 0 , sizeof(siginfo_t));
waitid(idtype , id , &info , WNOHANG);
if(info.si_pid == 0){
/*没有任何子进程的状态发生改变*/
}else{
/*有子进程的状态发生改变*/
}
#include
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
id_t id;
int status;
int ret;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}
//子进程
else if(pid == 0){
printf("I'm child, pid = %d\n", getpid());
exit(8); //子进程以_exit方式退出,退出状态的值为8
}
//父进程
else{
siginfo_t info;
//清空siginfo结构体
memset(&info , 0 , sizeof(siginfo_t));
ret = waitid(P_ALL , id , &info , WEXITED);
if(ret < 0){
perror("waitid error:");
}
//判断子进程是否以_exit方式退出
if(info.si_code == CLD_EXITED)
{
printf("si_code = CLD_EXITED\n");
}
//打印子进程退出状态的值
printf("si_status = %d\n" , info.si_status);
}
return 0;
}