所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口 。
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制 。
域指定套接字通信中使用的网络介质。最常见的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6),它是指 Internet 网络,许多 Linux 局域网使用的都是该网络,当然,因特网自身用的也是它。
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :
int socket(int domain, int type, int protocol);
/*
1.函数功能:创建套接字
2.参数:
int domain:套接字的域通常为 AF_INET(IPv4)或者AF_INET6(IPV6)
int type:套接字类型通常为 SOCK_STREAM、SOCK_DGRAM
int protocol:
0 :使用默认协议
IPPROTO_TCP:使用TCP协议
IPPROTO_UDP:使用UDP协议
3.返回值:
成功:返回套接字描述符
失败:-1
*/
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/*
1.函数功能:绑定套接字Socket与主机网络地址信息
2.参数:
int sockfd: 套接字描述符
const struct sockaddr *addr:主机地址信息,下文详解
socklen_t addrlen: 参数2的长度(字节)
3.返回值:
成功:0
失败:-1
*/
//以下主要摘自LINUX手册
typedef unsigned short int sa_family_t;
/* Structure describing a generic socket address.翻译:描述通用套接字地址的结构 */
struct sockaddr {
sa_family_t sa_family;//地址族
char sa_data[14];//14字节,包含套接字中的目标地址和端口信息
}
/* Structure describing an Internet socket address.翻译:描述Internet套接字地址的结构 */
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
char sin_zero[8];//占位不使用,用来与struct sockaddr对齐
};
/* Internet address */
struct in_addr {
/*uint32_t*/ in_addr_t s_addr;/* address in network byte order地址的网络字节序 */
};
/*sin_addr is the IP host address.
The s_addr member of struct in_addr contains the host interface address in network byte order.
翻译:sin_addr为主机IP地址。struct in_addr的s_addr成员以网络字节顺序包含主机接口地址*/
示例:
int sockfd;
struct sockaddr_in serverAddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* 填充struct sockaddr_in */
bzero(&serverAddr, sizeof(serverAddr));//初始化为0状态 主要是对成员sin_zero[8]清0
serverAddr.sin_family = AF_INET; //设置地址家族
serverAddr.sin_port = htons(SERV_PORT);//端口号1024-65535
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* 强制转换成struct sockaddr */
bind(sockfd, (struct sockaddr *) &serverAddr, sizeof(serverAddr));
//填充IP地址
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0.0 等号后面可以是htonl(0)或者0
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_aton("127.0.0.1",&serverAddr.sin_addr);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
//填充端口
serverAddr.sin_port = htons(1234);//端口号1024-65535
serverAddr.sin_port = htons(0);//随机端口 等号后面可以 0
相关函数:
1. inet_addr
in_addr_t inet_addr(const char *cp);
/*
功能:点分字符串格式地址转网络格式
参数:IPv4地址字符串例如"127.0.0.1"
返回值:
成功:返回网络字节序的地址用于赋值serverAddr.sin_addr.s_addr
失败:-1
*/
2.inet_ntoa 、inet_aton
char *inet_ntoa (struct in_addr in) //net to ascii
/*
功能:网络字节序地址转点分字符串格式地址
参数:传入通用的网络字节序地址struct in_addr sin_addr
返回值:
成功:返回指针指向IPv4点分字符串格式地址 例如"127.0.0.1"
失败:0
*/
int inet_aton(const char *cp, struct in_addr *inp); //ascii to net
/*
功能:点分字符串格式地址转网络格式地址
参数:
cp:IPv4点分字符串格式地址
inp:网络字节序地址struct in_addr sin_addr
返回值:
成功:非0
失败:0
*/
4.htons、htonl
uint16_t htons(uint16_t hostshort);//h host n net s short
uint32_t htonl(uint32_t hostlong);//h host n net l long
/*
功能:将主机字节序的short/long类型数据转为网络字节序类型数据
参数:
short类型数据/long类型
返回值:
成功:网络字节序类型数据
失败:-1
*/
5.inet_pton、inet_ntop
这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构的二进制值。
int inet_pton(int family, const char *strptr, void *addrptr);
//返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//返回值:若成功则为指向结构的指针,若出错则为NULL
NBO : 网络字节序
HBO : 主机字节序
LE little-endian:小端
BE big-endian:大端
tcp/ip规定它们的网络字节序都是大端字节序。主机字节序可能是大端也可能是小端,与主机的cpu有关,与操作系统无关考虑到与协议的一致以及与同类其它平台产品的互通,在程序中发数据包时,将主机字节序转换为网络字节序,收数据包处将网络字 节序转换为主机字节序。网络程序开发时 或是跨平台开发时 应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug。数据在传输的过程中,一定有一个标准化的过程,也就是说:
从主机a到主机b进行通信:a的主机字节序——网络字节序——b的主机字节序
int main()
{
union
{
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
if(sizeof(short)==2)
{
if(un.c[0] == 1 && un.c[1] == 2)
printf("Big-Endian\n");
else if(un.c[0] == 2 && un.c[1] == 1)
printf("Little-Endian\n");
else
printf("Unknown\n");
}
else
print("sizeof(short)=%d\n",sizeof(short));
exit(0);
}
在创建并绑定套接字之后,我们就可以尝试TCP、UDP通信了。
TCP/IP协议是一个协议簇。里面包括很多协议,UDP只是其中的一个。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
/*
功能:接收数据
参数:
int sockfd:socket函数的返回值,套接字描述符
void *buf:存放收到的数据
size_t len:参数2的大小
int flags:如果没有数据到来 阻塞等待还是不等待 0表示阻塞 MSG_DONTWAIT 不等待
struct sockaddr *src_addr:用于获取发送方的地址信息
socklen_t *addrlen:发送方地址信息长度 注意:传的实参必须初始化
返回值:
成功:返回实际收到的字节数
失败:-1
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
/*
功能:发送数据给对端
参数:
int sockfd:socket函数的返回值,套接字描述符
const void *buf:要发送的数据存放的地址
size_t len:参数2的大小
int flags:套接字缓存满 阻塞还是不阻塞 0表示阻塞 MSG_DONTWAIT 不阻塞
const struct sockaddr *dest_addr:目标端的地址信息
socklen_t *addrlen:目标端的地址信息
返回值:
成功:返回实际发送的字节数
失败:-1
*/
/***************************/
/* 服务器端 */
/***************************/
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
char buf_data[1024] = {};
/*创建套接字*/
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd:%d\n",sockfd);
/*定义网络地址结构体变量并填充*/
struct sockaddr_in myselfAddr;
myselfAddr.sin_family = AF_INET;
myselfAddr.sin_port = htons(6666);//把短整形转为网络格式
myselfAddr.sin_addr.s_addr = htonl(INADDR_ANY);//主机格式转网络格式
/*套接字与主机绑定*/
int ret_bind = bind(sockfd,(struct sockaddr*)&myselfAddr,sizeof(myselfAddr));
if(ret_bind == -1)
{
perror("bind");
close(sockfd);
exit(1);
}
/*缓存用于获取对端网络地址信息*/
struct sockaddr_in buf_sockaddr;
socklen_t buf_addrlen = sizeof(buf_sockaddr);
printf("等待客户端连接...\n");
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
pid_t pid = fork();
if(pid>0)
{
while(1)
{
bzero(buf_data,sizeof(buf_data));
gets(buf_data);
ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&buf_sockaddr,buf_addrlen);
if(ret_send == -1)
{
perror("sendto");
close(sockfd);
exit(1);
}
printf("我:%s\n",buf_data);
}
}
else if(pid == 0)
{
while(1)
{
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
}
}
else
{
perror("fork");
close(sockfd);
exit(1);
}
return 0;
}
/***************************/
/* 客户端 */
/***************************/
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
char buf_data[1024] = "\0";
/*创建套接字*/
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd:%d\n",sockfd);
/*输入并配置对端网络地址信息*/
short port;
char IP[20];
printf("输入对方IP:\n");
scanf("%s",IP);
getchar();
printf("输入对方端口号:\n");
scanf("%hd",&port);
getchar();
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);//短整型转为网络格式
serverAddr.sin_addr.s_addr = inet_addr(IP);//字符串格式转网络地址格式
/*缓存用于获取对端的网络地址信息*/
struct sockaddr_in buf_sockaddr;
socklen_t buf_addrlen = sizeof(buf_sockaddr);
pid_t pid = fork();
if(pid>0)
{
while(1)
{
/*发送信息*/
bzero(buf_data,sizeof(buf_data));
gets(buf_data);
ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
if(ret_send == -1)
{
perror("sendto");
close(sockfd);
exit(1);
}
printf("我:%s\n",buf_data);
bzero(buf_data,sizeof(buf_data));
}
}
else if(pid == 0)
{
/*接收信息*/
while(1)
{
bzero(buf_data,sizeof(buf_data));
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
}
}
else
{
perror("fork");
close(sockfd);
exit(1);
}
return 0;
}
int listen(int sockfd, int backlog);
/*
功能:使套接字进入被动监听状态
参数:
int sockfd: 需要进入监听状态的套接字
int backlog:请求队列的最大长度
返回值:
成功:0
失败:-1
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
功能:处理来自客户端的连接请求
参数:
int sockfd: 处于监听状态的套接字(服务器绑定的套接字也叫监听套接字)
struct sockaddr *addr:用于获取对端的地址信息
socklen_t *addrlen:参数2的大小
返回值:
成功:返回一个新的套接字(这个套接字与当前发起申请的客户端连接)
失败:-1
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
功能:客户端发起连接服务器请求
参数:
int sockfd:client套接字
const struct sockaddr *addr:对端(服务器)的地址信息
socklen_t *addrlen:参数2的大小
返回值:
成功:0
失败:-1
*/
建立好了 TCP 连接之后,我们就可以把得到的 sockfd 当作文件描述符来使用。
#include
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
需要注意 read 函数的返回值:
recv 和 send 函数提供了和 read 和 write 差不多的功能。前3个参数同read、write,第4
个参数用来控制读写操作。
#include
#include
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
/*
参数 int flags:
0:等同于write
MSG_DONTROUTE:告诉 IP 目的主机在本地网络上面,没有必要查找表,这个标志一般用网络诊断和路由程序里面
MSG_OOB:表示可以接收和发送带外的数据
MSG_DONTWAIT:仅本操作非阻塞(执行完恢复阻塞模式)
*/
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
/*
参数 int flags:
0:等同于read
MSG_PEEK:表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容
MSG_OOB:表示可以接收和发送带外的数据
MSG_DONTWAIT:仅本操作非阻塞(执行完恢复阻塞模式)
MSG_WAITALL:表示等到所有的信息到达时才返回。使用这个标志的时候 recv 会一直阻塞,直到指定的条件满足,或者是发生了错误:
1.当读到了指定的字节时,函数正常返回。返回值等于 len
2.当读到了文件的结尾时,函数正常返回。返回值小于 len
3.当操作发生错误时,返回 -1,且设置错误为相应的错误号 (errno)
*/
实现客户端与服务器端的对话,服务器端运行时命令行传参端口号,客户端运行时命令行传参服务器的IP和端口号
关于程序中用到的IO多路复用select函数,参考select函数详解
/***************************/
/* 服务器端 */
/***************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
void error_Handling(char* func,int retval);
void error_of_read(int retval,char* IP);
void sigFun(int sig);
int count = 0;
int main(int argc,char* argv[])
{
if(argc != 2)
{
printf("%s Port",argv[0]);
exit(1);
}
signal(SIGCHLD,sigFun);
char buf_data[1024] = {};
/*创建套接字——监听套接字*/
int listenfd = socket(AF_INET,SOCK_STREAM,0);
error_Handling("socket",listenfd);
/*绑定监听套接字与主机网络地址信息*/
int on = 1;
int ret_set = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));//地址复用
error_Handling("setsockopt",ret_set);
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret_bind = bind(listenfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
error_Handling("bind",ret_bind);
/*设置监听队列的大小*/
int ret_listen = listen(listenfd,10);
error_Handling("listen",ret_listen);
/*监听等待连接*/
struct sockaddr_in buf_addr;
socklen_t buf_addrlen = sizeof(buf_addr);
while(1)
{
printf("服务器持续监听中\n");
printf("连接服务器的客户端数量:%d\n",count++);
int newconfd = accept(listenfd,(struct sockaddr*)&buf_addr,&buf_addrlen);//返回分机套接字
error_Handling("accept",newconfd);
pid_t pid = fork();
error_Handling("fork",pid);
if(pid == 0)
{
printf("FATHERPID:%d CHILDPID:%d\n",getppid(),getpid());
printf("与IP:| %s |建立连接\n",inet_ntoa(buf_addr.sin_addr));
while(1)
{
/*在这里做一个IO多路复用*/
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(newconfd,&readfds);
int ret_select = select(newconfd+1,&readfds,NULL,NULL,NULL);
error_Handling("select",ret_select);
/*select返回表示有描述符就绪*/
if(FD_ISSET(0,&readfds))//检查标准输入是否被置位
{
ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));
error_Handling("read",ret_read);
ssize_t ret_write = write(newconfd,&buf_data,sizeof(buf_data));
error_Handling("write",ret_write);
printf("我:%s\n",buf_data);
bzero(&buf_data,sizeof(buf_data));
}
if(FD_ISSET(newconfd,&readfds))//检查套接字是否被置位
{
ssize_t ret_read = read(newconfd,&buf_data,sizeof(buf_data));
error_of_read(ret_read,inet_ntoa(buf_addr.sin_addr));
printf("%s:%s\n",inet_ntoa(buf_addr.sin_addr),buf_data);
bzero(&buf_data,sizeof(buf_data));
}
}
}
}
/*关闭套接字*/
//close(newconfd);
close(listenfd);
return 0;
}
void error_Handling(char* func,int retval)
{
if(retval == -1)
{
perror(func);
exit(1);
}
}
void error_of_read(int retval,char* IP)
{
if(retval<0)
{
perror("read");
exit(1);
}
if(retval == 0)
{
perror("read");
printf("%s 已断开连接,此进程结束\n",IP);
exit(1);
}
}
void sigFun(int sig)
{
wait(NULL);
count--;
printf("有子进程退出已经收尸\n");
}
/***************************/
/* 客户端 */
/***************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
void error_Handling(char* func,int retval);
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("%s IP Port\n",argv[0]);
exit(1);
}
char buf_data[1024] = {};
/*创建套接字*/
int sockfd = socket(AF_INET,SOCK_STREAM,0);
error_Handling("socket",sockfd);
/*连接*/
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(atoi(argv[2]));
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret_connect = connect(sockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
error_Handling("connect",ret_connect);
/*发送数据*/
while(1)
{
/*在这里做一个IO多路复用*/
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(sockfd,&readfds);
int ret_select = select(sockfd+1,&readfds,NULL,NULL,NULL);
error_Handling("select",ret_select);
/*select返回表示有描述符就绪*/
if(FD_ISSET(0,&readfds))//检查标准输入是否被置位
{
ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));
error_Handling("read",ret_read);
ssize_t ret_write = write(sockfd,&buf_data,sizeof(buf_data));
error_Handling("write",ret_write);
printf("我:%s\n",buf_data);
bzero(&buf_data,sizeof(buf_data));
}
if(FD_ISSET(sockfd,&readfds))//检查套接字是否被置位
{
ssize_t ret_read = read(sockfd,&buf_data,sizeof(buf_data));
error_Handling("read",ret_read);
printf("%s:%s\n",inet_ntoa(serverAddr.sin_addr),buf_data);
bzero(&buf_data,sizeof(buf_data));
}
}
/*关闭套接字*/
close(sockfd);
return 0;
}
void error_Handling(char* func,int retval)
{
if(retval == -1)
{
perror(func);
exit(1);
}
}
//把套接字设置成非阻塞模式
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
//非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源
阻塞模式下:
非阻塞模式下:
read/recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
阻塞模式下:
非阻塞模式下:
TCP发送数据的过程:首先,TCP是有链接的可靠传输协议,所谓可靠也就是说保证客户端发送的数据服务端都能够收到,并且是按序收到。
数据首先由应用程序缓冲区复制到发送端的输出缓冲区(位于内核),注意这个过程是用类似write功能的函数完成的。有的人通常看到write成功就以为数据发送到了对端主机,其实这是错误的,write成功仅仅表示数据成功的由应用进程缓冲区复制到了输出缓冲区。
然后内核协议栈将输出缓冲区中的数据发送到对端主机,注意这个过程不受应用程序控制,而是发送端内核协议栈完成,其中包括使用滑动窗口、拥塞控制等功能。
数据到达接收端主机的输入缓冲区,注意这个接收过程也不受应用程序控制,而是由接收端内核协议栈完成,其中包括发送ack确认等。
数据由套接字接收缓冲区复制到接收端应用程序缓冲区,注意这个过程是由类似read等函数来完成。
思考:如果TCP服务端一直sleep,客户端一直发送数据,会出现什么情况?
“华清远见” http://www.hqyj.com/学习更多编程知识