套接口即网络进程的ID;网络通信归根到底即为进程间的通信;套接字中包含了端口号,用来确定进程,一个端口号一次只能分配给一个进程,即端口号与进程是一一对应的;
一、套接字的结构
1、IPv4套接字地址结构
IPv4地址结构命名为sockaddr_in,定义在
头文件中,结构定义如下:
struct sockaddr_in {
sa_family_t sin_family; //IPV4协议为AF_INET,协议族
in_port_t sin_port; //16位端口号,网络字节序列
struct in_addr sin_addr; //32位IP地址
unsigned char sin_zero[8]; //备用域;
};
struct in_addr{
in_addr_t s_addr; //32位IP地址,网络字节序列
} ;
2、通用套接字地址结构
struct sockaddr{
sa_family_t sa_family;
char sa_data[14]
}
通用套接字结构本质上和上面的IPv4套接字是一致的,通用套接字只是在函数调用的过程中用于参数的传递,定义时用sockaddr_in定义,在函数调用时通过类型转换来使用。可以参考sockaddr和sockaddr_in的区别 。套接字结构除了这两种常见的外还有基于IPv6和Unix域的结构sockaddr_in6,sockaddr_un等。
二、相关处理函数
1、字节排序函数
#include
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值
uint32_t ntohl(uint32_t net32bitvalue); //返回主机字节序的值
其中,h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换,16bit和32bit分别用于端口号和IP地址。有时可以用任何地址则用INADDR_ANY,INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。
2、字节操作函数
字节操作函数主要是用于操作结构体中的某几个字节,在处理套接字结构中有IP地址等字段,包含0字节却不是C字符串,需要特定的函数来进行处理。
#include
void bzero(void *dest,size_t nbytes);
void bcopy(const void *src,void *dest,size_t nbytes);
int bcmp(const void *prt1,const void *ptr2,size_t nbytes); //相等返回0,否则非0
void *memset(void *dest,int c,size_t len);
void *memcpy(void *dest,const void *src,size_t nbytes);
int memcmp(const void *ptr1,const void *ptr2,size_t nbytes); //相等返回0,否则非0
以b打头的函数为支持套接口函数的系统所提供,mem为支持ANSI C库提供的函数;其中,bzero将指定数目的字节设置为0,bcopy将指定数目的字节从源字节串复制到目的字节串。bcmp比较两个字节串,相同返回0。memset将目标中指定数据的字节设置为指定的值(不一定是0),memcpy为复制,memcmp为比较,两者与前面类似。
3、地址转换函数
一般使用ASCII字符串表示IP地址,也就是点分十进制数表示(218.170.19.1),但在套接字中使用32位网络字节序,保存的是二进制值,因此必须进行地址转换。
#include
in_addr_t inet_addr(const char *straddr); //字符串有效返回网络字节序的IP地址,否则INADDR_NONE
int inet_aton(const char* straddr,struct in_addr *addrp);//字符串有效返回1,否则0
char* inet_ntoa(struct in_addr inaddr); //返回一个字符串指针
int inet_pton(int family, const char* str, void* addr);
//成功返回1,字符串无效返回0,出错返回-1
const char* inet_ntop(int family, const void* addr, char* str, size_t len);
//成功返回结果指针,出错返回NULL
/*说明
*后面两个函数为新函数,支持IPv4和IPv6,family用来指定:AF_INET,AF_INET6
*inet_ntop函数len为目标存储单元大小,str指针就是函数返回值
*/
inet_aton将一个字符串转换为32位网络字节序二进制值,用结构in_addr存储。inet_addr功能相同(废弃,不使用),inet_ntoa进行相反的操作。
4、字节流读取函数
在套接字通信中进行字节读取函数:read(),write()。与I/O中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少。
ssize_t write(int fd, const void*buf,size_t nbytes);
ssize_t read(int fd,void *buf,size_t nbyte);
/*说明
*函数均返回读或写的字节个数,出错则返回-1
*/
第一个将buf中的nbytes个字节写入到文件描述符fd中,成功时返回写的字节数。第二个为从fd中读取nbyte个字节到buf中,返回实际所读的字节数。详细应用说明参考使用read write 读写socket(套节字) 。
网络I/O还有一些函数,例如:recv()/send(),readv()/writev(),recvmsg()/sendmsg(),recvfrom()/sendto()等,后面进行详细介绍。
三、socket通信相关函数
1、socket 函数
为了执行网络I/O,进程必须做的第一件事就是执行socket函数,指定期望的通信协议类型。套接字是通信端点的抽象,实现端对端之间的通信,访问套接字需要套接字描述符。套接字描述符通过socket 函数获得,这样才能对套接字进行操作。
/*
* 函数功能:创建套接字描述符;
* 返回值:若成功则返回套接字非负描述符,若出错返回-1;
* 函数原型:
*/
#include
int socket(int family, int type, int protocol);
/*
* 说明:
* socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;
* family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:
* (1)AF_INET IPv4因特网域
* (2)AF_INET6 IPv6因特网域
* (3)AF_UNIX Unix域
* (4)AF_ROUTE 路由套接字
* (5)AF_KEY 密钥套接字
* (6)AF_UNSPEC 未指定
*
* type确定socket的类型,常用类型如下:
* (1)SOCK_STREAM 有序、可靠、双向的面向连接字节流套接字
* (2)SOCK_DGRAM 长度固定的、无连接的不可靠数据报套接字
* (3)SOCK_RAW 原始套接字
* (4)SOCK_SEQPACKET 长度固定、有序、可靠的面向连接的有序分组套接字
*
* protocol指定协议,常用取值如下:
* (1)0 选择type类型对应的默认协议
* (2)IPPROTO_TCP TCP传输协议
* (3)IPPROTO_UDP UDP传输协议
* (4)IPPROTO_SCTP SCTP传输协议
* (5)IPPROTO_TIPC TIPC传输协议
*
*/
2、connect 函数
TCP用户用connect函数与服务器建立连接。
/*
* 函数功能:建立连接,即客户端使用该函数来建立与服务器的连接;
* 返回值:若成功则返回0,出错则返回-1;
* 函数原型:
*/
#include
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
/*
* 说明:
* sockfd是系统调用的套接字描述符,即由socket函数返回的套接字描述符;
* servaddr是目的套接字的地址,该套接字地址结构必须包含目的IP地址和目的端口号,即想与之通信的服务器地址;
* addrlen是目的套接字地址的大小;
*
* 如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址,即内核会确定源IP地址,并选择一个临时端口号作为源端口号,即客户端connect前不一定要调用bind()函数;
*/
3、bind 函数
bind()函数用于将一个网络地址赋予一个套接字,因为套接字在创建之初是没有地址的需要进行赋值,这里的地址一般为网络IP和端口号。
/*
* 函数功能:将协议地址绑定到一个套接字;其中协议地址包含IP地址和端口号;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
* 说明:
* sockfd 为套接字描述符;
* addr是一个指向特定协议地址结构的指针,通用套接字结构;
* addrlen是地址结构的长度;
*/
对于 TCP 协议,调用 bind 函数可以指定一个端口号,或指定一个 IP 地址,也可以两者都指定,还可以都不指定。若 TCP 客户端或服务器端不调用bind 函数绑定一个端口号,当调用connect 或 listen 函数时,内核会为相应的套接字选择一个临时端口号。一般 TCP 客户端使用内核为其选择一个临时的端口号,而服务器端通过调用bind 函数将端口号与相应的套接字绑定。进程可以把一个特定的 IP 地址捆绑到它的套接字上,但是这个 IP 地址必须属于其所在主机的网络接口之一。对于 TCP 客户端,这就为在套接字上发送的 IP 数据报指派了源 IP 地址。对于 TCP 服务器端,这就限定该套接字只接收那些目的地为这个 IP 地址的客户端连接。TCP 客户端一般不把 IP 地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源 IP 地址,而所用外出接口则取决于到达服务器端所需的路径。若 TCP 服务器端没有把 IP 地址捆绑到它的套接字上,内核就把客户端发送的 SYN 的目的 IP 地址作为服务器端的源 IP 地址。
4、listen 函数
listen函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
/*
* 函数功能:接收连接请求;
* 若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include
int listen(int sockfd, int backlog);
/*
* sockfd是套接字描述符;
* backlog是该进程所要入队请求的最大请求数量;
*/
内核为任何一个给定监听套接字维护两个队列:
5、accept 函数
accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
/* 函数功能:从已完成连接队列队头返回下一个已完成连接;若已完成连接队列为空,则进程进入睡眠;
* 返回值:若成功返回套接字描述符,出错返回-1;
* 函数原型:
*/
#include
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
/*
* 说明:
* 参数 cliaddr 和 addrlen 用来返回已连接的对端(客户端)的协议地址;
*
* 该函数返回套接字描述符,该描述符连接到调用connect函数的客户端;
* 这个新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个链接,
* 而是继续保持可用状态并接受其他连接请求;
* 若不关心客户端协议地址,可将cliaddr和addrlen参数设置为NULL,否则,在调用accept之前,应将参数cliaddr设为足够大的缓冲区来存放地址,
* 并且将addrlen设为指向代表这个缓冲区大小的整数指针;
* accept函数返回时,会在缓冲区填充客户端的地址并更新addrlen所指向的整数为该地址的实际大小;
* 若没有连接请求等待处理,accept会阻塞直到一个请求到来;
该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
6、fork 函数
/* 函数功能:创建子进程;
* 返回值:
* (1)在子进程中,返回0;
* (2)在父进程中,返回新创建子进程的进程ID;
* (3)若出错,则范回-1;
* 函数原型:
*/
#include
pid_t fork(void);
/* 说明:
* 该函数调用一次若成功则返回两个值:
* 在调用进程(即父进程)中,返回新创建进程(即子进程)的进程ID;
* 在子进程返回值是0;
* 因此,可以根据返回值判断进程是子进程还是父进程;
*/
任何一个子进程只有一个父进程,一个父进程可以有多个子进程,fork函数在调用父进程中返回一次,返回新派生的进程ID号,在子进程又返回一次,返回值为0,可以根据返回值判断进程是父进程还是子进程。下面为与进程处理相关的几个简单函数介绍。
/* getppid
* 函数功能:获取当前进程父进程id;
* 返回值:父进程id
* 函数原型:
*/
#include
pid_t getppid(void);
/* 说明:
* 在调用中不能返回错误;
* getpid(void)为获取当前调用进程的id;
*/
/* kill
* 函数功能:给进程发送信号
* 返回值:成功返回0,失败返回-1;
* 函数原型:
*/
#include
int kill(pid_t pid, int sig)
/* 说明:
* pid为被发送信号的进程id,sig为信号编号,pid的取值有几种情况:
* 1、pid > 0 :将此信号发送给进程ID为pid的进程;
* 2、pid = 0 :将此信号发送给当前进程组内所有的进程;
* 3、pid < 0 :将此信号发送给进程组识别码为pid绝对值的所有进程;
* 4、pid = -1:将信号广播传送给系统内所有的进程;
*/
/* pause
* 函数功能:使当前进程睡眠,直到接收到信号;
* 返回值:-1,EINTR(接收到中断信号);
* 函数原型:
*/
#include
int pause(void);
7、exec 函数
存放在硬盘上的可执行文件能够被Unix系统执行的唯一方法是由一个现有进程调用6个exec函数中的一个。exec把当前进程映像替换成新的程序文件,而该新程序通常从main函数开始执行,进程ID并不改变,一般称调用exec的进程为调用进程。
/* exec 函数 */
/*
* 函数功能:把当前进程替换为一个新的程序文件,进程ID并不改变;
* 返回值:若出错则返回-1,若成功则不返回;
* 函数原型:
*/
#include
int execl(const char *pathname, const char *arg, ...);
int execv(const char *pathnam, char *const argv[]);
int execle(const char *pathname, const char *arg, ... , char *const envp[]);
int execve(const char *pathnam, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg, ...);
int execvp(const char *filename, char *const argv[]);
/* 6 个函数的区别如下:
* (1)待执行的程序文件是由文件名还是由路径名指定;
* (2)新程序的参数是一一列出还是由一个指针数组来引用;
* (3)把调用进程的环境传递给新程序还是给新程序指定新的环境;
* 只有execve是内核中的系统调用,其它5个函数都是调用execve的库函数,关系如下图
*/
8、close 函数
/*
* 函数功能:关闭套接字,若是在 TCP 协议中,并终止 TCP 连接;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include
int close(int sockfd);
/*
* close之后套接字描述符不能再由调用进程使用,即不能作为read和write参数;
* 在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共,
* 享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相,
* 应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断,
* 连过程。
* shutdown函数也有类似的功能;
*/
9、shutdown 函数
/*
* 函数功能:关闭套接字;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include
int shutdown(int sockfd, int howto);
/*
* sockfd为需要关闭的套接字描述符,howto用来指定函数的行为;
* howto的3种取值方式:
* 1.SHUT_RD:值为0,关闭连接的读这一半;
* 2.SHUT_WR:值为1,关闭连接的写这一半;
* 3.SHUT_RDWR:值为2,连接的读和写都关闭。
*/
close与shutdown区别:
close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。
而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。
linux网络编程之shutdown() 与 close()函数详解 ;
10、getsockname ,getpeername 函数
/*
* 函数功能:获取已绑定到一个套接字的地址;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);
/*
* 说明:
* 调用该函数之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小;
* 返回时,该整数会被设置成返回地址的大小,如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错;
*/
/*
* 函数功能:获取套接字对方连接的地址;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);
/*
* 说明:
* 该函数除了返回对方的地址之外,其他功能和getsockname一样;
*/
当不调用bind或调用bind没有指定本地IP地址和端口时,可以调用函数getsockname来返回内核分配的本地IP地址和端口号,还可以获取到套接口的协议簇。当一个新的连接建立时,服务器端也可以调用getsockname来获取分配给此连接的本地IP地址;当服务器端的一个子进程调用函数exec启动执行时,只能调用getpeername来获取客户端IP地址和端口号。
四、并发服务器
大多数TCP服务器是并发的,它们为每一个待处理的客户连接调用fork派生一个子进程,当一个连接建立时,accept返回,服务器调用fork函数新建一个子进程,由子进程处理与客户的连接,父进程则可以在监听套接字上再次调用accept来处理下一个客户的连接。
所有的客户端和服务器都是从调用socket开始的,它返回一个描述符,客户端调用connect,服务器调用bind,listen,accept。建立连接之后I/O函数进行数据的传递。通信结束一般用close关闭套接字。
部分参考资料:
网络编程学习笔记一:Socket编程;
基本 TCP 套接字编程讲解;
套接字编程基础;
UNIX网络编程卷1:套接字联网API;
五、简单客户/服务程序
下面为在UNIX网络编程卷1:套接字联网API中摘录的一个简单TCP网络通信程序,有适当修改。
/*
* 服务器端;
* 实现从客户端获取数据并重新写回服务器;
* 每一个客户端访问都通过子进程处理;
* 提供并发服务;
*/
#include
#include
#include
#include
#include
#include
#include
#include
void str_echo(int sockfd); //回射函数声明
int main(int argc, char** argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0); //获取监听套接字描述符
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666); //服务器套接字赋值
bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)); //绑定套接字
listen(listenfd, 5); //监听,5用来规定最大连接数
for(;;)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &clilen); //接受连接
if((childpid = fork()) == 0 )
{//创建子进程,由子进程处理连接
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[256] = ""; //空字符串
while( (n = read(sockfd, buf, 256)) > 0)
{
write(sockfd, buf, n);
printf("receive message: %s\n",buf);
if(n < 0 )
printf("read error!");
bzero(buf, 256); //字符串置0
}
}
/*
* 说明:
* 这里buf在定义时赋值为一个空字符串,是为了在下面的printf输出中不产生乱码,
* 因为read只读取获取的字节个数,当不足buf的256个时,buf后面为空,printf
* 输出buf会把buf后面非接收字节输出(乱码);
* 没有处理子进程中止信号,子进程进入僵死状态;
*/
/*
* 客户端;
* 首先建立和服务器的连接,然后开始信息的传输;
* 从标准输入端获取传输消息,发送到服务器;
* 从服务器接收信息,输入到屏幕;
*/
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char** argv)
{
int sockfd;
struct sockaddr_in servaddr; //服务器套接字
char* ips = "192.168.110.128"; //服务器IP
sockfd = socket(AF_INET, SOCK_STREAM, 0); //获取客户端套接字描述符
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666); //端口号
if(inet_aton(ips, &servaddr.sin_addr) <= 0)
{
printf("inet_aton error!");
exit(0);
}
if(connect(sockfd, (struct sockaddr*)& servaddr, sizeof(servaddr)) < 0)
{ //连接到服务器
printf("connect error!");
exit(0);
}
char sendline[256], recvline[256] = "";
printf("Input message to be send:\n");
while(fgets(sendline, 256, stdin) != NULL)
{ //从标准输入获取发送信息
if(write(sockfd, sendline, strlen(sendline)) < 0)
{//发送消息到服务器
printf("write message error!");
exit(0);
}
if(read(sockfd, recvline, 256) < 0)
{//接收服务器返回的消息
printf("read message error");
exit(0);
}
printf("%s",recvline); //输出接收信息
bzero(&recvline, 256);
printf("Input message to be send:\n");
}
close(sockfd);
exit(0);
}
/*
* 说明:
* 客户端套接字没有bind其地址,由内核自动添加;
* 当服务器关闭,客户端不会第一时间获知,再次发送信息才会读取到服务器发送的FIN;
* 可以不断进行信息传输;
*/
/*
* 测试输出
* 观察客户端和服务器的输出情况
*/
/* 客户端 */
[centos@localhost Documents]$ ./a.out //启动客户端连接
Input message to be send:
hello,serve! //输入
hello,serve! //接收输出
Input message to be send:
test for client
test for client
Input message to be send:
...
/* 服务器端 */
receive message: hello,serve!
receive message: test for client
...
/*
* 说明:
* 先启动服务器,在启动客户端进行连接并发送数据;
* 客户端可以通过下面方式中止;
* ctrl-c :发送 SIGINT 信号,终止一个进程;
* ctrl-d :不发送信号, 表示输入EOF字符,程序中fgets会触发中止命令;
* ctrl-z :发送SIGSTOP信号,挂起一个进程,可以用fg使之回到前台,不会终止客户端;
*/