目录
socket、bind、listen、accept、connect
socket编程(一)
什么是socket
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”
我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具
socket可以看出是用户进程与内核网络协议栈的编程接口
IPv4套接口地址结构
IPv4套接口地址结构通常也称为”网际套接口地址结构“,它以”sockaddr_in“命名,定义在头文件<netinet/in.h>中。其POSIX定义如下:
/* sockaddr_in */
struct in_addr {
in_addr_t s_addr; /* 23 bits IPv4 address */
}; /* network byte ordered */
struct sockaddr_in {
uint8_t sin_len; /* length of structure(16) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-bit TCP or UDP port number */
/* network byte ordered */
struct in_addr sin_addr; /* 32-bit IPv4 address */
/* network byte ordered */
char sin_zero[8]; /* unused */
};
/*
sin_len成员
表示地址结构体的长度,它是一个无符号的八位整数。
需要强调的是,这个成员并不是地址结构必须有的。
假如没有这个成员,其所占的一个字节被并入到sin_family成员中;
同时,在传递地址结构的指针时,结构长度需要通过另外的参数来传递。
sin_family成员
指代的是所用的协议族,在有sin_len成员的情况下,它是一个8位的无符号整数;
在没有sin_len成员的情况下,它是一个16位的无符号整数。
由于IP协议属于TCP/IP协议族,所以在这里该成员应该赋值为“AF_INET”。
sin_port成员
表示TCP或UDP协议的端口号,它是一个16位的无符号整数。
它是以网络字节顺序(大端字节序)来存储的。
in_addr成员
用来保存32位的IPv4地址,它同样是以网络字节顺序来存储的。
sin_zero成员
是不使用的,通常会将它置为0,它的存在只是为了与通用套接字地址结构struct sockaddr在内存中对齐。
*/
网络字节序
字节序
大端字节序:
高位字节数据存放在低地址处,低位数据存放在高地址处;
小段字节序:
高位字节数据存放在高地址处,低位数据存放在低地址处
主机字节序
不同的主机有不同的字节序, x86位小端字节序 ARM字节序可配置
网络字节序:
TCP/IP协议传输数据时,字节序默认大端。
字节序转换函数
函数原形:
// hton* 主机字节转网络字节序 uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); // ntoh* 网络字节序转主机字节序 uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
函数功能:
字节序转换函数
所属头文件:
#include<arpa/inet.h>
参数说明:
//上述函数中 h代表host n代表network s代表short l代表long
地址转换函数
函数原形:
int inet_aton (const char *__cp, struct in_addr *__inp); in_addr_t inet_addr (const char *__cp); char * inet_ntoa (struct in_addr __in);
函数功能:
//以上三个函数在点分十进制数串(如“127.0.0.1")和32位网络字节序二进制值之间转换IPv4地址。 inet_aton将__cp指向的字符串转成网络序的地址存在__inp指向的地址结构。 成功返回1,否则返回0。 (据书中所说,如果__inp指针为空,那么该函数仍然对输入字符串进行有效性检查但是不存储任何结果) inet_addr功能和inet_aton类似, 但是inet_addr出错时返回INADDR_NONE常值(通常是32位均为1的值), 这就意味着至少有一个IPv4的地址(通常为广播地址255.255.255.255)不能由该函数处理。 建议使用inet_aton代替inet_addr。 inet_ntoa将网络序二进制IPv4地址转换成点分十进制数串。 该函数的返回值所指向的字符串驻留在静态内存中。 这意味着该函数是不可重入的。 同时我们也该注意到该函数以一个结构体为参数而不是常见的以一个结构体指针作为参数。
所属头文件:
#include <arpa/inet.h>
套接字类型
- (1)SOCK_STREAM:流式套接字,提供面向连接、可靠的数据传输服务,数据按字节流、按顺序收发,保证在传输过程中无丢失、无冗余。TCP协议支持该套接字。
- (2)SOCK_DGRAM:数据报套接字,提供面向无连接的服务,数据收发无序,不能保证数据的准确到达。UDP协议支持该套接字。
- (3)SOCK_RAW:原始套接字。允许对低于传输层的协议或物理网络直接访问,例如可以接收和发送ICMP报文。常用于检测新的协议。
socket编程(二)
TCP客户/服务器模型
回射客户/服务器
socket、bind、listen、accept、connect
socket、
函数原形:
int socket(int domain, int type, int protocol);
函数功能:
socket()打开一个网络通讯端口,如果成功的话,返回一个文件描述符
所属头文件:
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
返回值
成功返回一个新的文件描述符,失败返回-1,设置errno
参数说明:
domain: AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址 AF_INET6 与上面类似,不过是来用IPv6的地址 AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用 type: SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。 SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。 SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序 protocol: 0 默认协议
bind、
函数原形:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数功能:
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
所属头文件:
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
返回值
成功返回0,失败返回-1, 设置errno
参数说明:
sockfd: socket文件描述符 addr: 构造出IP地址加端口号 addrlen: sizeof(addr)长度
listen、
函数原形:
int listen(int sockfd, int backlog);
函数功能:
listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。 查看系统默认backlog cat /proc/sys/net/ipv4/tcp_max_syn_backlog 内核为任何一个给定的监听套接字维护两个队列: (1)未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RECV状态 (2)已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。
所属头文件:
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
返回值
成功返回0,失败返回-1。
参数说明:
sockfd: socket文件描述符 backlog: 排队建立3次握手队列和刚刚建立3次握手队列的链接数和
accept、
函数原形:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数功能:
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
所属头文件:
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
返回值
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
参数说明:
sockdf: socket文件描述符 addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号 addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
connect
函数原形:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数功能:
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于 bind的参数是自己的地址,而connect的参数是对方的地址。
所属头文件:
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>
返回值
成功返回0,失败返回-1,设置errno
参数说明:
sockdf: socket文件描述符 addr: 传入参数,指定服务器端地址信息,含IP地址和端口号 addrlen: 传入参数,传入sizeof(addr)大小
socket编程(三)
SO_REUSEADDR
setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前
所有TCP服务器都应该指定本套接字选项,以防止当套接字处于TIME_WAIT时bind()失败的情形出现;
SO_REUSEADDR的能力:
//(1)SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;
//【即便TIME_WAIT状态存在,服务器bind()也能成功】
//(2)允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
//(3)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可;
//(4)SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,如果传输协议支持,
//同样的IP地址和端口还可以绑定到另一个套接字上;一般来说本特性仅支持UDP套接字[TCP不行];
点对点聊天程序实现
实现要点
- 客户端
客户端分为两个进程:一个进程用于接收服务器进程发送过来的数据;另一个进程用于发送- 服务器
服务器分为两个进程:一个进程用于接收客户端进程发送过来的数据;另一个进程用于发送
实现代码
- p2pcli.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#define MAX_BUF 1024
void handle(int sig)
{
printf("recv : %d\n",sig);
exit(0);
}
int main()
{
int clt_fd;
struct sockaddr_in serv_addr;
char buf[MAX_BUF]={0};
char addr_dst[INET_ADDRSTRLEN] = {0};
ssize_t ret;
pid_t pid;
signal(SIGUSR1,handle);
clt_fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == clt_fd)
{
perror("socket");
exit(-1);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8001);
serv_addr.sin_addr.s_addr = inet_addr("192.168.1.110");
if(connect(clt_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1)
{
perror("connect");
exit(-1);
}
printf("Connect successfully\t%s at PORT %d\n",inet_ntop(AF_INET,&serv_addr.sin_addr,addr_dst,sizeof(addr_dst)),ntohs(serv_addr.sin_port));
pid = fork();
if(pid > 0)//父进程负责给服务器端发送数据
{
printf("Please input the alphabet:");
while(fgets(buf,sizeof(buf),stdin) != NULL)//从标准输入获取数据
{
write(clt_fd,buf,strlen(buf));//发送数据
memset(buf,0,sizeof(buf));//缓冲器清理
printf("Please input the alphabet:");
}
}
else if(pid == 0)//子进程负责从服务器端获取数据
{
while(1)
{
ret = read(clt_fd,buf,sizeof(buf));//读数据
if(ret == 0)
{
/*如果在读的过程中,对方已关闭,TCP/IP协议会返回一个0数据包*/
printf("server close\n");
break;
}
else if(ret < 0)
{
perror("read");
exit(-1);
}
fputs(buf,stdout);//打印内容
memset(buf,0,sizeof(buf));
}
close(clt_fd);
kill(getppid(),SIGUSR1);//发送信号给父进程--通知他死期已到
}
else if(pid == -1)//创建子进程错误
{
perror("fork");
close(clt_fd);
exit(-1);
}
return 0;
}
- p2psvr.c
-
/*server03*/ #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> /* superset of previous */ #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <signal.h> #define MAX_CLIENT 10 #define MAX_READ 1024 void handle(int sig)//辅助杀死子进程或者父进程 { printf("recv : %d\n",sig); exit(0); } int main() { int serv_fd,con_fd;//服务器端至少要有两个套接字文件描述符--一个用来监听,一个/其余多个用来和客户端通信 struct sockaddr_in serv_addr;//IPV4套接字结构体--服务器 struct sockaddr_in clt_addr;//IPV4套接字结构体--客户端 char addr_dst[INET_ADDRSTRLEN] = {0};//存储客户端IP地址 char buf[MAX_READ]={0};//应用程序自己的缓冲区 int optvar;//地址复用使用的参数 pid_t pid;//子进程PID socklen_t addr_len; ssize_t ret ; signal(SIGUSR1,handle);//注册新号和处理函数 serv_fd = socket(AF_INET,SOCK_STREAM,0);//建立套接字 if(-1 == serv_fd) { perror("socket"); exit(-1); } if(setsockopt(serv_fd, SOL_SOCKET,SO_REUSEADDR,&optvar,sizeof(optvar)) == -1 )//地址复用 { perror("setsockopt"); exit(-1); } /*设置地址*/ bzero(&serv_addr,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(8001); serv_addr.sin_addr.s_addr = htons(INADDR_ANY); if(bind(serv_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) == -1)//绑定端口 { perror("bind"); exit(-1); } /*一旦调用listen函数--套接字就会变成被动套接字--用来监听客户端,让客户端连接他 被动套接字--只能接受连接,不能主动发送连接 做了两个队列: 一个已经完成三次握手,建立连接的队列--客户端发connect请求被响应,已经成功完成连接 一个是未完成成三次握手的队列--正在握手 */ if(listen(serv_fd,MAX_CLIENT)== -1)//开始监听 { perror("listen"); exit(-1); } addr_len = sizeof(clt_addr); printf("Accepting connections ...\n"); if((con_fd = accept(serv_fd,(struct sockaddr *)&clt_addr,&addr_len)) == -1)//由于是点对点--所以只能处理一个客户端的连接请求 { perror("accept"); exit(-1); } printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&clt_addr.sin_addr,addr_dst,sizeof(addr_dst)),ntohs(clt_addr.sin_port)); pid = fork();//创建子进程 if(pid > 0)//父进程负责读取客户端数据 { while(1) { ret = read(con_fd,buf,sizeof(buf));//读取数据 if(ret == 0) { /*如果在读的过程中,对方已关闭,TCP/IP协议会返回一个0数据包*/ printf("client close\n"); break; } else if(ret < 0) { perror("read"); exit(-1); } fputs(buf,stdout);//打印内容 memset(buf,0,sizeof(buf)); } close(serv_fd);//关闭套接字 close(con_fd); kill(pid,SIGUSR1);//发送信号给子进程--通知他死期已到 } else if(pid == 0)//子进程负责向客户端写入数据 { printf("Please input alphabet:"); while(fgets(buf,sizeof(buf),stdin) != NULL)//从标准输入获取数据 { write(con_fd,buf,strlen(buf));//给客户端发送数据 memset(buf,0,sizeof(buf));//缓冲区清理 printf("Please input alphabet:"); } } else if(pid == -1)//创建子进程错误 { perror("fork"); close(serv_fd); close(con_fd); exit(-1); } return 0; }