进行套接字编程需要指定套接字的地址作为参数,例如bind()函数原型
int bind(int sockfd, /*套接字文件描述符*/
const struct sockaddr *myaddr, /*套接字地址结构*/
socklen_t addrlen /*套接字地址结构长度*/
);
不同的协议族有不同的地址结构定义方式。地址结构通常以sockaddr_开头,每一个协议族有一唯一的后缀,例如 以太网协议的地址结构为sockaddr_in,Netlink协议的地址结构为sockaddr_nl。
通用套接字地址结构可与不同协议族之间的地址结构进行强制转换,定义如下:
struct sockaddr{
sa_family_t sa_family; /*协议族 sa_family_t类型为unsigned short*/
char sa_data[14]; /*协议族数据*/
}
struct sockaddr_in{
u8 sin_len; /*结构struct sockaddr_in的长度 */
u8 sin_family; /*协议族 通常为AF_INET 表示IP*/
u16 sin_port; /*16位的端口号, 网络字节序*/
struct in_addr sin_addr; /*IP地址32位*/
char sin_zero[8]; /*未用*/
}
struct in_addr的成员变量用于表示IP地址,定义如下:
struct in_addr{
u32 s_addr; /*32位IP地址,网络字节序(大端存储)*/
}
结构struct sockaddr 和sockaddr_in的关系图如下:
由于二者结构大小完全一致,在进行套接字编程时,通常利用struct sockaddr_in进行设置,然后强制转换为结构struct sockaddr类型。
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* netlink协议族类型 一般设置为AF_NETLINK (跟AF_INET对应)*/
unsigned short nl_pad; /* 填充zero */
__u32 nl_pid; /* port ID (通信端口号)*/
__u32 nl_groups; /* 组播 multicast groups mask */
};
TCP网络编程存在两种模式
客户端模式:根据目的服务器的地址和端口进行连接, 向服务器发送请求并对服务器的响应进行数据处理
服务器模式:创建服务程序,等待客户端的连接请求,接收到用户的连接请求后,根据用户的请求进行处理。
客服端和服务器端TCP网络编程流程如图:
套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。这个过程中的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
函数原型如下:
#include
#include
int socket(int domin, int type, int protocol);
/*
domin 套接字通信的协议族
type 套接字通信的协议类型
protocol 指定某个协议的特定类型,如果某个协议只有一种类型,protocol设置为0
*/
/*TCP例子*/
int mysockfd = socket(AF_INET, SOCK_STREAM, 0);
该函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回-1。
参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。以太网中应该PF_INET 这个域。在程序设计的时候会发现有的代码使用了AF_INET 这个值,在头文件 中AF_INET 和PF_INET的值是一致的。
参数type用于设置套接字通信的类型,主要有SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)等。
并不是所有的协议族都实现了这些协议类型,例如,AF_INET 协议族就没有实现 SOCK_SEQPACKET 协议类型。
type类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行。一旦连接,可以使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内仍然没有接收完毕,可以将这个连接认为已经死掉。
type类型为 SOCK_DGRAM和SOCK_RAW这两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接收数据, recvfrom()接收来自指定IP地址的发送方的数据。
type类型为SOCK_PACKET是一种专用的数据包,它直接从设备驱动接收数据。
参数protocol用于指定某个协议的特定类型,即type类型中的某个类型。通常某个协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得,具体值和含义如下表。通常情况下造成函数socket()失败的原因是输入的参数错误造成的,例如某个协议不存在等,这时需要详细检查函数的输入参数。由于函数的调用不一定成功,在进行程序设计的时候,一定要检查返回值。
图中用户调用函数sock=socket(AF_INET,SOCK_STREAM,0),这个函数会调用系统调 用函数sys_socket(AF_INET, SOCK_STREAM,0)(在文件net/socket.c中)。系统调用sys_socket()分为两部分,一部分生成内核socket结构(注意与应用层的socket函数是不同的),另一部分与文件描述符绑定,将绑定的文件描述符值传给应用层。内核sock结构如下(在文件linux/net.h中) :
struct socket{
socket_state 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结构。
在建立套接字文件描述符成功后,需要对套接字进行地址绑定,将套接字与一个地址结构进行绑定。包括IP地址、端口地址及协议类型等参数。 bind()函数将长度为addlen的struct sockadd类型的参数my_addr与sockfd绑定在一起,将sockfd绑定到某个端口上,如果使用connect()函数则没有绑定的必要。
函数原型如下:
#include
#include
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
参数sockfd是用socket()函数创建的文件描述符。
参数my_addr是指向一个结构为sockaddr参数的指针, sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要先将地址结构中的IP地址、端口、类型等进行设置后才能进行绑定,绑定后才能将套接字文件描述符与地址等结合在一起。
参数addrlen是my_addr结构的长度,可以设置成 sizeof(struct sockaddr)。使用 sizeof(struct sockaddr)来设置addlen是一个良好的习惯,虽然一般情况下使用AF_INET来设置套接字的类型和其对应的结构,但是不同类型的套接字有不同的地址描述结构,如果对地址长度进行了强制的指定,可能会造成不可预料的结果。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败, errno的错误值如下图所示
函数bind()是应用层函数,要使函数生效,需要将相关的参数传递给内核并进行处理。应用层的bind()函数与内核层之间的函数过程如上图所示,图中是一个AF_INET族函数进行绑定的调用过程。
应用层的函数bind(sockfd,(struct sockaddr*)&my_addr, sizeof(struct sockaddr))调用系统函数过程sys_bind(sockfd,(struct sockaddr*)&my_addr, sizeof(struct sockaddr))。sys_bind()函数首先调用函数sockfd_lookup_light()来获得文件描述符sockfd对应的内核struct sock结构变量,然后调用函数move_addr_to_kernel()将应用层的参数my_addr复制进内核,放到address变量中。内核的sock结构是在socket()函数时根据协议生成的,它绑定了不同协议族的bind() 函数的实现方法,在AF_INET族中的实现函数为inet_bind(),即会调用AF_INET族的bind()函数进行绑定处理。
由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。
函数原型如下:
#include
int listen(int sockfd, int backlog);
/*
sockfd 为socket()创建的套接字文件描述符
backlog表示等待队列的长度
运行成功返回0,运行失败返回-1,并设置errno值 错误代码含义如下图
*/
在接受一个连接之前,需要用listen()函数来侦听端口,listen()函数中参数backlog的参数表示在accept()函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户 端会返回一个ECONNREFUSED错误。
listen()函数仅对类型为SOCK_STREAM或者SOCK_SEQPACKET的协议有效,例如, 如果对一个SOCK_DGRAM的协议使用listen(),将会出现错误 errno应该为值EOPNOTSUPP,表示此socket不支持listen()操作。大多数系统的设置为20,可以将其设置修改为5或者10,根据系统可承受负载或者应用程序的需求来确定。
应用层listen()和内核层listen()的关系如图所示,应用层的listen()函数对应于系统 调用sys_listen(O)函数。sys_listen()函数首先调用sockfd_lookup_light()函数获得sockfd对应的内核结构struct socket,查看用户的backlog设置值是否过大,如果过大则设置为系统默 认最大设置。然后调用抽象的listen()函数,这里指的是AF_INET 的listen()函数inet_listen()。inet_listen()函数首先判断是否合法的协议族和协议类型,然后更新socket的状态值为TCP_LISTEN,然后为客户端的等待队列申请空间并设定侦听端口。
当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,直到使用服务器处理接收请求。函数accept()成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据收发。
函数原型:
#include
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
通过accept()函数可以得到成功连接客户端的IP地址、端口和协议族等信息,这个信息是通过参数addr获得的。当accept()返回的时候,会将客户端的信息存储在参数addr中。 参数addrlen表示第2个参数(addr)所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。需要注意的是在accept 中addrlen参数是一个指针而不是结构, accept0将这个指针传给TCP/IP协议栈。
accpet()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()返回的新套接字文件描述符来进行的,而不是通过建立套接字时的文件描述符,这是在程序设计的时候需要注意的地方。如果accept()函数发生错误, accept()会返回-1。错误类型如图所示:
应用层的accept()函数对应内核层的sys_accept()系统调用函数。函数sys_accept()查找文件描述符对应的内核socket结构、申请一个用于保存客户端连接的新的内核socket结构、执行内核接受函数、获得客户端的地址信息、将连接的客户端地址信息复制到应用层的用户、返回连接客户端socket对应的文件描述符。
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_userO函数将信息复制到应用层空间,具体的地址由用户传入的参数来确定。
客户端在建立套接字之后,不需要进行地址绑定,直接连接网络服务器。connect()函数原型如下:
#include
#include
int connect(int sockfd, struct sockaddr* serv_addr, int addrlen);
参数sockfd是建立套接字时返回的套接字文件描述符,它是由系统调用socket()返回的。参数serv_addr是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址以及协议类型。参数addrlen 表示第二个参数内容的大小,可以使用sizeof(struct sockaddr)获得。connect()函数的返回值在成功时为0,当发生错误的时候返回-1,可以查看errno获得错误的原因,错误值如下图。
当服务器端在接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相对应的内核函数。
函数原型
#include
ssize_t write (int fd,const void *buf,size_t count)
下面是一个向套接字文件描述符中写入数据的例子,将缓冲区data的数据全部写入套接字文件描述符s中,返回值为成功写入的数据长度。
int size;
char data[1024];
size = write(s, data, 1024);
与写入数据类似,使用read()函数可以从套接字描述符中读取数据。当然在读取数据之前,必须建立套接字并连接。
函数原型
#include
ssize_t read(int fd, void *buf ,size_t count)
读取数据的方式如下所示,从套接字描述符s中读取1024个字节,放入缓冲区data中, size变量的值为成功读取的数据大小。
int size;
char data[1024];
size = read(s, data, 1024);
当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接,内核释放相关的资源。
#include
int close(int sockfd)
函数shutdown()可以使用更多方式来关闭连接,允许单方向切断通信或者切断双方的通信。
函数原型如下,第一个参数s是切断通信的套接口文件描述符,第二个参数how表示切断的方式。
#include
int shutdown (int sockfd, int how);
函数shutdown()用于关闭双向连接的一部分,具体的关闭行为方式通过参数的how设置来实现。可以为如下值:
SHUT_RD:值为0,表示切断读,之后不能使用此文件描述符进行读操作。
SHUT_WR:值为1,表示切断写,之后不能使用此文件描述符进行写操作。
SHUT_RDWR:值为2,表示切断读写,之后不能使用此文件描述符进行读写操作,与close)函数功能相同。
函数shutdown()如果调用成功则返回0,如果失败则返回-1,通过errno可以获得错误的具体信息,错误值含义如下图:
程序分为服务器端和客户端,客户端连接服务器后从标准输入读取输入的字符串,发送给服务器:服务器接收到字符串后,发送接收到的总字符串个数给客户端;客户端将接收到的服务器的信息打印到标准输出。
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 4567 /*服务器侦听端口地址*/
#define BACKLOG 5 /*侦听队列长度*/
int main(int argc, char *argv[]){
int client_fd, server_fd; /*client_fd为客户端的socket描述符,server_fd为服务器的socket描述符*/
struct sockaddr_in server_addr; /*服务器套接字地址结构*/
struct sockaddr_in client_addr; /*客户端套接字地址结构*/
int ret; /*返回值*/
pid_t pid; /*分叉进程ID*/
}
server_fd = socket(PF_INET, SOCK_STREAM, 0); /*建立流式套接字*/
if(server_fd < 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(SERV_PORT); /*服务器端口*/
ret = bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); /*绑定地址到套接字描述符*/
if(ret < 0){
printf("bind error!\n");
return -1;
}
ret = listen(server_fd, BACKLOG); /*设置侦听队列*/
if(ret < 0){
printf("listen error!\n");
return -1;
}
在主循环过程中为了方便处理,每个客户端的连接请求服务器会分叉一个进程进行处理。函数fork()出来的进程继承了父进程的属性,例如套接字描述符,在子进程和父进程中都有一套。为了防止误操作,在父进程中关闭了客户端的套接字描述符,在子进程中关闭了父进程中的侦听套接字描述符。一个进程中的套接字文件描述符的关闭,不会造成套接字的真正关闭,因为仍然有一个进程在使用这些套接字描述符,只有所有的进程都关闭了这些描述符, Linux内核才释放它们。在子进程中,处理过程通过调用函数 process_conn_server()来完成。
for(;;){
int addrlen = sizeof(struct sockaddr);
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen); /*接收客服端连接*/
if(client_fd < 0){ /*出错*/
continue; /*结束本次循环*/
}
pid = fork(); /*建立子进程处理该连接*/
if(pid == 0){
close(server_fd); /*子进程中关闭服务器的的侦听*/
process_conn_server(client_fd); /*处理连接*/
exit(0); /*退出子线程*/
}else{
close(client_fd); /*在父进程中关闭客户端的连接*/
}
}
服务器端对客户端连接的处理过程如下,先读取从客户端发送来的数据,然后将接收到的数据个数发送给客户端。
void process_conn_server(int client_fd){ /*服务器建立子进程处理客户端请求*/
ssize_t size = 0;
char buffer[1024]; /*数据缓冲区*/
for(;;){
size = read(client_fd, buffer, 1024); /*从套接字中读取数据放到缓冲区buffer中*/
if(size == 0){ /*没有数据,准备结束该子线程*/
return;
}
sprintf(buffer, "%d bytes altogether!\n", size); /*构建响应字符,为接收到客户端字节的数量*/
write(client_fd, buffer, strlen(buffer) + 1); /*发给客户端*/
}
}
void singal_handle(int sign){
wait(NULL);
}
signal(SIGCHLD, singal_handle); /*放到main函数中 ,用于接收子进程退出信号SIGCHID,执行信号处理函数singal_handle,调用wait 回收子线程*/
信号是发生某件事情时的一个通知,有时候也将称其为软中断。信号将事件发送给相关的进程,相关进程可以对信号进行捕捉并处理。信号的捕捉由系统自动完成,信号处理函数的注册通过函数signal()完成。 这个函数向信号signum注册一个void(*sighandler_t)(int)类型的函数,函数的句柄为handler。当进程中捕捉到注册的信号时,会调用响应函数的句柄handler。信号处理函数在处理系统默认的函数之前会被调用。
信号处理函数原型:
#include
typedef void (*sighandler_t)(int); /*函数指针 参数为int 返回值为void*/
sighandler_t signal(int signum, sighandler_t handler);
signum信号量:
#define SIGHUP 1 //hang up 挂断控制终端或进程
#define SIGINT 2 //interrupt 来自键盘的中断
#define SIGQUIE 3 //quit 退出
#define SIGILL 4 //illeagle 非法指令
#define SIGTRAP 5 //trap 跟踪断点
#define SIGABORT 6 //Abort 异常结束
#define SIGIOT 6 //IO trap 异常
#define SIGUNUSED 7 //Unused 没有使用
#define SIGFPE 8 //FPE 协处理器出错
#define SIGKILL 9 //kill 强迫终止
#define SIGUSR1 10 //use1 用户信号1,进程可使用
#define SIGSEGV 11 //segment violation 无效内存引用
#define SIGUSR2 12 //user2 用户信号2,进程可使用
#define SIGPIPE 13 //pipe 管道写出错
#define SIGALRM 14 //alarm 实时定时器报警
#define SIGTERM 15 //terminate 进程终止
#define SIGSTKFLT 16 //stack Fault 栈出错
#define SIGCHLD 17 //child 子进程停止
#define SIGCONT 18 //continue 回复进程继续
#define SIGSTOP 19 //stop 停止进程执行
#define SIGTSTP 20 //tty stop tty发出停止进程
#define SIGTTIN 21 //tty in 后台进程请求输入
#define SIGTTOU 22 //tty out 后台进程请求输出
#define SA_NOCLDSTOP 1 //进程处于停止状态,就不对sigchild进行处理
#define SA_NOMASK 0X40000000
//不阻止在指定的信号处理程序中再收到该信号
#define SA_ONESHOT 0X80000000
//信号句柄一旦被调用就恢复到默认处理句柄
#define SIG_BLOCK //在阻塞信号集中加上给定的信号集
#define SIG_UNBLOCK//从阻塞信号集中删除指定的信号集
#define SIG_SETMASK//设置阻塞信号集(信号屏蔽码)
#define SIG_DFL ((void(*)(int))0) //默认信号处理程序
#define SIG_IGN ((void (*)(int))1)//忽略信号处理程序
#include
#include
#include
#include
#include
#include
#include
#include "tcp_process.h"
#define SERV_PORT 4567 /*服务器侦听端口地址*/
int main(int argc, char *argv[]){
int sockfd; /*socket描述符*/
struct sockaddr_in server_addr; /*服务器地址结构*/
int ret; /*返回值*/
sockfd = socket(PF_INET, SOCK_STREAM, 0); /*建立流式套接字*/
if(sockfd < 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 = inet_addr("127.0.0.1"); /*服务器地址*/
server_addr.sin_port = htons(SERV_PORT); /*服务器端口*/
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)); /*连接服务器*/
process_conn_client(sockfd); /*客服端处理过程*/
close(sockfd);
}
客户端从标准输入读取数据到缓冲区buffer中,发送到服务器端。然后从服务器端读取服务器的响应,将数据发送到标准输出。
void process_conn_client(int sockfd){
ssize_t size = 0;
char buffer[1024];
for(;;){
size = read(0, buffer, 1024); /*从标准输入中读取数据放到缓冲区buffer中, 0代表标准输入文件描述符*/
buffer[size - 1] = '\0'; /*删除换行符*/
size = strlen(buffer); /*真实标准输入大小,不包括换行符(回车键)*/
if(size == 0){ /*标准输入仅包含换行符(回车键)直接退出*/
write(sockfd, buffer, size);
break;
}
if(size > 0){
write(sockfd, buffer, size); /*发送服务器*/
size = read(sockfd, buffer, 1024); /*从服务器读取数据*/
write(1, buffer, size); /*写到标准输出, 1为标准输出文件描述符*/
}
}
}
all:client server #all规则,它依赖于client和server规则
client: tcp_process.o tcp_client.o #client规则,生成客户端可执行程序
gcc -o client tcp_process.o tcp_client.o
server: tcp_process.o tcp_server.o #server规则,生成服务器端可执行程序
gcc -o server tcp_process.o tcp_server.o
clean: #清理规则,删除client、server和中间文件
rm -rf client server *.o
仅输入回车键,关闭client1
参考文献 :Linux 网络编程
github传送