总体流程如下图server(服务器) ——》 client(客户):
下面会细致展开
1、网卡接收数据
计算机由CPU、存储器(内存)、网络接口等部件组成。
下图展示了网卡接收数据的过程。
这个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存。
1、计算机收到了对端传送的数据(步骤①);
2、数据经由网卡传送到内存(步骤②);
3、然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能;
注:
蓝色区域
里面的等待队列
:就是用户空间进程调用recv函数(读取数据)
请求读取内核缓冲区内的数据,由于缓冲区数据没有准备好,所以处于等待状态(又称为阻塞状态)。(这里看不懂就不用深究啦)
你有这样的疑问吗?操作系统如何知道网络数据对应于哪个socket套接字?
因为一个socket对应着一个端口号
,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。
你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”
那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。
Socket就像一个电话插座,负责连通两端的电话,进行点对点通信,让电话可以进行通信,端口就像插座上的孔,端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上,创建一个Socket实例开始监听后,这个电话插座就时刻监听着消息的传入,谁拨通我这个“IP地址和端口”,我就接通谁。
socket 和 文件描述符之间的关系?
套接字也是文件。具体数据传输流程如下:
当server端
监听到有连接时,应用程序会请求内核创建Socket
;
Socket
创建好后会返回一个文件描述符
给应用程序;
当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口
等在内核维护的一个ipcb双向链表
中找到对应的Socket,并将数据包赋值到该Socket的缓冲区
;
应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据
注意:
Socket双向链表
,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口
从对应的链表中找到其对应的Socket
,并会将数据拷贝到Socket的缓冲区
,等待应用程序读取。(上面有图)想了解文件描述符(fd)是什么请参考此链接:
文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?
总结:
所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket()
,它返回套接字描述符 (socket descriptor)
,然后你再通过它来进行send() 和 recv()调用。
struct sockaddr
这个结构 为许多类型的套接字储存套接字地址信息:
struct sockaddr {
sa_family_t sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
};
sa_family
成员是地址族类型( sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族( protocol family,也称 domain,如下表
sa_data
成员用于存放 socket
地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下表所示。
由表5-2可见,14字节的 sa_data
根本无法完全容纳
多数协议族的地址值。
为了处理struct sockaddr
,程序员创造了一个并列的结构: struct sockaddr_in
(“in” 代表 “Internet”。)
struct sockaddr_in:
#include
struct sockaddr_in {
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
};
//port和addr 分开储存在两个变量中
/* Internet 地址 (存放32 位IP地址) */
struct in_addr {
unsigned long s_addr;
};
所以:
sockaddr_in变量赋值
后,强制类型转换
后传入用sockaddr做参数
的函数:实例
#include
#include
#include
#include
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in mysock;
sockfd = socket(AF_INET,SOCK_STREAM,0); //获得fd
bzero(&mysock,sizeof(mysock)); //初始化结构体
mysock.sin_family = AF_INET; //设置地址家族
mysock.sin_port = htons(800); //设置端口
mysock.sin_addr.s_addr = inet_addr("192.168.1.0"); //设置地址
bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr); /* bind的时候进行转化 */
... ...
return 0;
}
1、大端、小端字节序
“大端”和”小端”表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序;具体的说:
如下图:当以不同的存储方式,存储数据为0x12345678时:
网络字节序:大端字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题:
UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待
,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节
所以:网络字节序就是大端字节序
, 有些系统的本机字节序是小端字节序, 有些则是大端字节序, 为了保证传送顺序的一致性, 所以网际协议使用大端字节序来传送数据
。
字节序转换函数
#include
//将主机字节序转换为网络字节序
unit32_t htonl (unit32_t hostlong);
unit16_t htons (unit16_t hostshort);
//将网络字节序转换为主机字节序
unit32_t ntohl (unit32_t netlong);
unit16_t ntohs (unit16_t netshort);
说明:h -----host;n----network ;s------short;l----long。
htons()--"Host to Network Short"
htonl()--"Host to Network Long"
ntohs()--"Network to Host Short"
ntohl()--"Network to Host Long"
为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?
答案是: sin_addr
和 sin_port
分别封装在包的 IP
和 UDP
层。因此,它们必须要 是网络字节顺序
。但是 sin_family
域只是被内核 (kernel)
使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上
,它们可以是本机字节顺序。
IP 地址如何处理:地址转换函数
IP地址的三种表示格式及在开发中的应用
1)点分十进制表示格式
2)网络字节序格式
3)主机字节序格式
用IP地址127.0.0.1
为例:
第一步 127 . 0 . 0 . 1 把IP地址每一部分转换为8位的二进制数。
第二步 01111111 00000000 00000000 00000001 = 2130706433 (主机字节序)
然后把上面的四部分二进制数从右往左按部分重新排列,那就变为:
第三步 00000001 00000000 00000000 01111111 = 16777343 (网络字节序)
1、
函数inet_addr(),
将IP地址从 点数格式转换成无符号长整型。使用方法如下:
函数原型
in_addr_t inet_addr(const char *cp);
转换网络主机地址(点分十进制)为网络字节序二进制值,
使用
ina.sin_addr.s_addr = inet_addr("132.241.5.10");
现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?
2、
你就要用到函数 inet_ntoa()
(“ntoa"的含义是"network to ascii”),就像这样:
函数原型
char* inet_ntoa(struct in_addr in);
参数:
struct in_addr
{
union
{
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
ULONG S_addr;
} S_un;
};
使用
SOCKADDR_IN sock;
sock.sin_family = AF_INET;
//将字符串转换为in_addr类型
sock.sin_addr.S_un.S_addr = inet_addr("192.168.1.111");
sock.sin_port = htons(5000);
//将in_addr类型转换为字符串
printf("inet_ntoa ip = %s\n",inet_ntoa(sock.sin_addr));
结果输出:
inet_ntoa ip = 192.168.1.111
注意:
inet_ntoa()
将结构体in_addr
作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的固定的指针
,所以每次调用 inet_ntoa()
,它就将覆盖上次调用时所得的IP地址
。例如:
char *a1, *a2;
……
a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */
printf("address 1: %s\n",a1);
printf("address 2: %s\n",a2);
输出如下:
address 1: 132.241.5.10
address 2: 132.241.5.10
socket()、bind()、listen()
完成初始化后,调用accept()
阻塞等待;客户端Socke
t对象调用connect()
向服务器发送了一个SYN并阻塞
;第一次握手
,即发送SYN和ACK
应答;connect()
返回,再发送一个ACK
给服务器;服务器Socket
对象接收客户端第三次握手ACK
确认,此时服务端从accept()
返回,建立连接。UNIX/Linux的一个哲学是:所有东西都是文件。 socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的 socket系统调用可创建一个 socket:
#include
#include
int socket(int domain, int type, int protocol);
返回值:
参数说明:
1、domain参数
:告诉系统使用哪个底层协议族。对TCP/IP
协议族而言,该参数应该设置为PF_INET( Protocol Family of Internet,用于IPv4)
或 PF_INET6(用于IPv6)
:对于UNIX本地域协议族而言,该参数应该设置为 PF_UNIX
。
2、type参数
:指定服务类型。服务类型主要有SOCK_STREAM
服务(流服务,TCP)和SOCK_UGRAM
(数据报,UDP)服务。对TCP/IP
协议族而言,其值取SOCK_STREAM
表示传输层使用TCP协议
,取 SOCK_DGRAM
表示传输层使用UDP协议。
3、protocol参数
:是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0
,表示使用默认协议。
参数更详细说明请参考
如果说服务器通过sten调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接
函数原型
#include
#include
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
返回值:
成功时返回0。一旦成功建立连接, sockfd就唯一地标识了这个连接,客户端就可以通过读写 sockfd来与服务器通信。
失败则返回-1,并设置erno。其中两种常见的ermo是 ECONNREFUSED
和 ETIMEDOUT,
它们的含义如下:
1、 ECONNREFUSED,目标端口不存在,连接被拒绝。
2、ETIMEDOUT,连接超时。
参数说明:
sockfd
参数:由 socket系统调用返回一个 socket.serv_addr
参数:是服务器监听的 socket地址, 即目的地端口和 IP 地址的数据结构 struct sockaddr
。addrlen
参数:则指定这个地址的长度,即sizeof(struct sockaddr)
#include
#include
#include
#define DEST_IP "132.241.5.10"
#define DEST_PORT 23
main()
{
int sockfd;
struct sockaddr_in dest_addr; /* 目的地址*/
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查 */
dest_addr.sin_family = AF_INET; /* host byte order */
dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */
/* don't forget to error check the connect()! */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
……
}
connect工作方式:阻塞+非阻塞
1、阻塞模式下,connect的返回结果
客户端调用connect()函数将激发TCP的三路握手过程,但仅在连接建立成功或出错时才返回(这一点和非阻塞区分很重要)
。
syn分节的响应
,则返回ETIMEOUT
错误;调用connect函数时,内核发送一个syn,若无响应则等待6s后再发送一个
,若仍然无响应则等待24s后在发送一个
,若总共等待75s
后仍未收到响应则返回本错误;RST
,则表明该服务器在我们指定的端口上没有进程在等待与之连接,这是一种硬错误,客户一收到RST马上返回ECONNREFUSED
错误; 产生RST的三种情况:
syn
在中间的某个路由器上引发了目的不可达icmp
错误,则认为是一种软错误。客户主机内核保存该消息,并按照第一种情况的时间间隔继续发送syn,咋某个规定时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH
或者ENETUNREACH
错误返回给进程;2、非阻塞工作模式
EINPROCESS
错误,但TCP通信的三路握手过程正在进行,所以可以使用select函数来检查这个连接是否建立成功。
int socfd = socket();
//set non-blocking
int ret = connect(sockfd, ...);
if(ret < 0)
{
return -1;
}
if(errno == EINPRPGRESS)
{
fd_set rdset, wrset;
FD_SET(sockfd, rdset);
FD_SET(sockfd, wrset);
ret = select(sockfd+1, &rdset, &wrset, NULL, timeout);
if(FD_ISSET(sockfd, &wrset))
{
if(FD_ISSET(sockfd, rdset))
{
//清楚sock error
}
//成功连接
return sockfd.
}
}
非阻塞connect的意义
非阻塞connect的意义在于提高并发度。阻塞connect下,完成一个三次握手需要耗费一个RTT时间。RTT时间波动很大。从局域网内的几时毫秒到广域网的几十秒。阻塞模式下,进程被connect阻塞住,什么都干不了。非阻塞下,我们可以让select或者epoll来监听listenfd,直到完成三次连接再继续进行数据的手法。
linux-socket connect阻塞和非阻塞模式 示例
对文件的读写操作read和wrie同样适用于 socket但是 socket编程接口提供了几个专门用于 socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
int send( SOCKET s,char *buf,int len,int flags );
int recv( SOCKET s, char *buf, int len, int flags)
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。recv函数,它并不是直接从网络中获取数据,而是从输入缓冲区中读取数据。
int recv( SOCKET s, char *buf, int len, int flags)
返回值:
参数说明:
recv函数的执行流程
1、当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,
2、如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
,
3、如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。
4、当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。 )
5、recv函数仅仅是copy数据,真正的接收数据是协议来完成的,recv函数返回其实际copy的字节数。
6、如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
int send( SOCKET s,char *buf,int len,int flags );
返回值:
参数说明:
send函数的执行流程
待发送数据的长度len
和套接字s的发送缓冲的 长度
,
len大于s
的发送缓冲区的长度,该函数返回SOCKET_ERROR
;len小于或者等于s
的发送缓冲区的长度,那么send先检查协议 是否正在发送s的发送缓冲中的数据
,
比较s的发送缓冲区的剩余空间和len
,
len大于剩余空间大小
,send就一直等待协议把s的发送缓冲中的数据发送完
,len小于剩余空间大小
,send就仅仅把buf中的数据copy到剩余空间里
(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。注意
负责将数据写入输出缓冲区
,数据从输出缓冲区发送到目标主机是由TCP协议完成的
。数据写入到输出缓冲区之后,send函数就可以返回了,数据是否发送出去,是否发送成功,何时到达目标主机,都不由它负责了,而是由协议负责。 char buffer[1024];
int len;
if ((len=recv(events[i].data.fd,buffer,sizeof(buffer), 0))>0)
{
send(events[i].data.fd,"Welcome to My server\n",21,0);
printf("%s fd %d \n",buffer,events[i].data.fd);
//close(client_fd);
}else{
printf("client offline with: "
"clientfd = %d \n",
events[i].data.fd);
}
关闭一个连接实际上就是关闭该连接对应的 socket,这可以通过如下关闭普通文件描述系统调用来完。
#include
int close( int fd );
返回值:
参数说明:
fd的引用计数减1
。只有当fd的引用计数为0
时,才真正关闭连接
。如果无论如何都要立即终止连接(而不是将 socket的引用计数减1),可以使用如下的shutdown系统调用,并且如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭。
#include
int shutdown(int sockfd,int how);
返回值
参数说明:
SHUT_RD(0)
:关闭sockfd上的读功能
,此选项将不允许sockfd进行读操作。即该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。SHUT_WR(1)
:关闭sockfd的写功能
,此选项将不允许sockfd进行写操作,即进程不能在对此套接字发出写操作。SHUT_RDWR(2)
:关闭sockfd的读写功能,
相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。注意:
应用场景:优雅的关闭
通常来说,socket是双向的,即数据是双向通信的。但有些时候,你会想在socket上实现单向的socket,即数据往一个方向传输。单向的socket便称为半开放Socket。要实现半开放式,需要用到shutdown()函数。
一般来说,半开放socket
适用于以下场合:
1、close
2、shutdown
更多区别请参考
篇幅问题:下一个博客继续说明,具体结构如下
网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!
server.c
1.服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333
#define BACKLOG 10
#define MAX_CONNECTED_NO 10
#define MAXDATASIZE 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;
char buf[MAXDATASIZE];
/*创建socket*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1){
perror("socket");
exit(1);
}
printf("socket success!,sockfd=%d\n",sockfd);
/*设置服务器sockaddr_in结构*/
server_sockaddr.sin_family=AF_INET;
server_sockaddr.sin_port=htons(SERVPORT);
server_sockaddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
/*绑定socket和端口*/
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1){
perror("bind");
exit(1);
}
printf("bind success!\n");
/*监听客户端请求*/
if(listen(sockfd,BACKLOG)==-1){
perror("listen");
exit(1);
}
printf("listening....\n");
/*接受客户端请求*/
if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1){
perror("accept");
exit(1);
}
/*接收客户端信息*/
if((recvbytes=recv(client_fd,buf,MAXDATASIZE,0))==-1){
perror("recv");
exit(1);
}
printf("received a connection :%s\n",buf);
/*关闭socket*/
close(sockfd);
}
client.c
2.客户端
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc,char *argv[]){
int sockfd,sendbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
/*argc<2,表示没有输入主机名,主机句是IP地址形式,如“192.168.1.1”*/
if(argc < 2){
fprintf(stderr,"Please enter the server's hostname!\n");
exit(1);
}
/*获取主机名,地址解析函数*/
if((host=gethostbyname(argv[1]))==NULL){
perror("gethostbyname");
exit(1);
}
/*创建socket*/
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
perror("socket");
exit(1);
}
/*设置serv_addr结构参数*/
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr=*((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
/*向服务器请求连接,serv_addr是服务器端地址*/
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1)
{
perror("connect");
exit(1);
}
/*发送消息给服务器,此时可以在服务器端看到"hello"字样*/
if((sendbytes=send(sockfd,"hello",5,0))==-1){
perror("send");
exit(1);
}
/*关闭连接*/
close(sockfd);
}
编译运行:开两个终端
#gcc server.c -o server
#./server //此时服务器端在监听
#gcc client.c -o client
#./client yourIP //客户端向服务器端发送“hello",服务器端监听终止
1、https://bbs.gameres.com/thread_842984_1_1.html
2、https://www.cnblogs.com/kefeiGame/p/7246942.html
3、《高性能网络编程》——游双
5、https://zhuanlan.zhihu.com/p/109826876
6、https://www.cnblogs.com/xingguang1130/p/11643446.html
7、https://zhuanlan.zhihu.com/p/112312104
8、https://www.cnblogs.com/wanpengcoder/p/5356776.html