本文主要介绍Linux Socket编程,网络之间的通信依赖于Socket。Socket介绍的比较详细,有兴趣的同学可以了解一下。
我们非常清楚信息交流的价值。进程如何在网络中通信?比如我们每天打开浏览器浏览网页时,浏览器进程是如何与web服务器进行通信的?用QQ聊天时,QQ进程如何与服务器或好友所在的QQ进程进行通信?这一切都靠插座?什么是插座?插座有哪些类型?还有socket的基本功能,本文要介绍的。本文的主要内容如下:
1、网络中进程之间如何通信?
本地进程间通信(IPC)有多种方式,但可以归纳为以下四类:
消息传递(管道、FIFO、消息队列)
同步(互斥、条件变量、读写锁、文件和写记录锁、信号量)
共享内存(匿名和命名)
远程过程调用(Solaris门和Sun RPC)
但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信。首先要解决的问题是如何唯一标识一个进程,否则通信将无法进行!进程可以由进程PID在本地唯一标识,但在网络中不可行。事实上,TCP/ip协议家族已经帮助我们解决了这个问题。网络层的IP地址可以唯一标识网络中的主机,而传输层的协议端口可以唯一标识主机中的应用(进程)。这样,三元组(ip地址、协议、端口)可以用来标识网络中的进程,网络中的进程通信可以使用这个标志与其他进程进行交互。
使用TCP/IP协议的应用通常使用API:UNIX BSD的socket和UNIX System V(已淘汰)的TLI来实现网络进程间的通信。目前几乎所有的应用都采用socket,现在是互联网时代,进程通信无处不在,所以我说“万物皆socket”。
2、什么是Socket?
我们已经知道网络中的进程通过socket进行通信,那么socket是什么呢?Socket起源于Unix,Unix/Linux的一个基本哲学就是“一切都是文件”,可以在“打开-读写/读-关闭”的模式下操作。我的理解是,socket是这种模式的一种实现,socket是一种特殊的文件,有些Socket函数就是在它上面的操作(读/写IO,打开和关闭)。我们将在后面介绍这些函数。
socket一词的起源
在网络领域的第一次应用是在1970年2月12日发布的IETF RFC33中,作者是斯蒂芬卡尔、史蒂夫克罗克和温顿瑟夫。根据美国计算机历史博物馆的记录,克罗克写道:“命名空间的元素可以被称为套接字接口。一个套接字接口构成一个连接的一端,一个连接完全可以由一对套接字接口来定义。”计算机博物馆补充道:“这比BSD中socket接口的定义早了大约12年。”
3、socket的基本操作
由于socket是“开-写/读-关”模式的实现,所以socket提供了对应这些操作的功能接口。以TCP为例介绍几种基本的socket接口函数。
3.1、socket()函数
int socket(int域,int类型,int协议);
socket函数对应的是普通文件的打开操作。普通的文件打开操作返回一个文件描述符,socket()用来创建一个套接字描述符,唯一标识一个套接字。这个套接字描述符与文件描述符相同,并且在后续操作中使用。把它作为一个参数,通过它可以进行一些读写操作。
就像您可以向fopen传递不同的参数值来打开不同的文件一样。创建套接字时,还可以指定不同的参数来创建不同的套接字描述符。套接字函数的三个参数是:
域:协议域,也称为协议族。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或AF_Unix、Unix域套接字)、AF_ROUTE等。协议决定了套接字的地址类型,通信时必须使用相应的地址。例如,AF_INET确定ipv4地址(32位)和端口号(16位)的组合,AF_UNIX确定应该使用绝对路径名作为地址。
类型:指定套接字类型。常用的套接字类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。(插座有哪些类型?)。
协议:顾名思义,就是指定协议。常用的协议有IPPROTO_TCP、IPPTOTO _ UDP、IPPROTO_SCTP、IPPROTO_TIPC等。分别对应TCP传输协议,UDP传输协议,STCP传输协议,TIPC传输协议(这个协议我会单独讨论!)。
注意:不是以上类型和协议可以随意组合,比如SOCK_STREAM不能和IPPROTO_UDP组合。当协议为0时,将自动选择该类型对应的默认协议。
当我们调用socket创建套接字时,返回的套接字描述符存在于地址族(AF_XXX)空间中,但是没有具体的地址。如果要给它分配一个地址,必须调用bind()函数,否则当你调用connect()和listen()时,系统会自动随机分配一个端口。
3.2、bind()函数
如上所述,bind()函数将地址族中的特定地址分配给套接字。比如对应AF_INET和AF_INET6就是给socket分配ipv4或者ipv6地址和端口号的组合。
int bind(int sockfd,const struct sockaddr *addr,socklen _ t addrlen);
该函数的三个参数是:
sockfd: socket描述符,由socket()函数创建,唯一标识一个套接字。bind()函数将一个名称绑定到这个描述符。
addr:指向要绑定到sockfd的协议地址的const struct sockaddr *指针。根据创建套接字时的地址协议系列,此地址结构是不同的。例如,ipv4对应于:
struct sockaddr_in {
sa _ family _ t sin _ family/*地址族:AF_INET */
in _ port _ t sin _ port/*按网络字节顺序排列的端口*/
结构in _ addr sin _ addr/*互联网地址*/
};
/*互联网地址。*/
结构输入地址{
uint32 _ t s _ addr/*按网络字节顺序排列的地址*/
};
Ipv6对应于:
struct sockaddr_in6 {
sa _ family _ t sin6 _ family/* AF_INET6 */
in _ port _ t sin6 _ port/*端口号*/
uint32 _ t sin6 _ flowinfo/* IPv6流信息*/
struct in6 _ addr sin6 _ addr/* IPv6地址*/
uint32 _ t sin6 _ scope _ id/*作用域ID(2.4中的新功能)*/
};
结构in6_addr {
无符号字符S6 _ addr[16];/* IPv6地址*/
};
Unix域对应于:
#定义UNIX路径最大值108
struct sockaddr_un {
sa _ family _ t sun _ family/* AF_UNIX */
char sun _ PATH[UNIX _ PATH _ MAX];/*路径名*/
};
addrlen:它对应于地址的长度。
通常服务器启动时会绑定一个众所周知的地址(如ip地址、端口号)提供服务,客户可以通过它连接服务器;但是,客户端不需要指定,系统会自动分配一个端口号和它自己的ip地址的组合。这就是为什么服务器通常在listen之前调用bind(),而客户端不会。相反,系统会在connect()时随机生成一个。
网络字节序与主机字节序
主机端就是我们通常所说的大端和小端模式:不同的CPU有不同的端类型。这些端序是指整数在内存中存储的顺序,称为主机端序。引用的大端序和小端序的定义如下:
A) Little-Endian表示低位字节在存储器的低位地址端放电,高位字节在存储器的高位地址端放电。
B) Big-Endian是指高位字节在内存的低位地址端放电,低位字节在内存的高位地址端放电。
网络字节序:
4字节32位值按以下顺序传输:先0 ~ 7位,再8 ~ 15位,再16 ~ 23位,最后24 ~ 31位。这种传输顺序称为大端字节序。因为TCP/IP头中的所有二进制整数都要求在网络中按此顺序传输,所以也叫网络字节序。字节序,顾名思义就是大于一个字节类型的数据在内存中的存储顺序,一个字节的数据是没有顺序的。因此,在将一个地址绑定到套接字时,请先将主机端序转换为网络端序,不要假设主机端序使用Big-Endian作为网络端序。这个问题造成了血案!因为公司代码中的这个问题,导致很多莫名其妙的问题,所以请记住不要对主机字节顺序做任何假设,一定要转换成网络字节顺序,赋给socket。
3.3、listen()、connect()函数
如果你是服务器,在调用socket()和bind()后会调用listen(),如果客户端调用connect()发出连接请求,服务器会收到这个请求。
int listen(int sockfd,int backlog);
int connect(int sockfd,const struct sockaddr *addr,socklen _ t addrlen);
listen函数的第一个参数是要监控的套接字描述符,第二个参数是相应套接字可以排队的最大连接数。socket()函数创建的socket默认为主动类型,listen函数将socket改为被动类型,等待客户的连接请求。
connect函数的第一个参数是客户端的套接字描述符,第二个参数是服务器的套接字地址,第三个参数是套接字地址的长度。客户端通过调用connect函数与TCP服务器建立连接。
3.4、accept()函数
TCP服务器依次调用socket()、bind()和listen()后,会监听指定的套接字地址。TCP依次调用socket()和connect()后,向TCP服务器发送连接请求。TCP服务器监听到这个请求后,会调用accept()函数来接收请求,这样就建立了连接。然后就可以开始网络I/O操作了,类似于普通文件的读写I/O操作。
int accept(int sockfd,struct sockaddr *addr,socklen _ t * addrlen);
accept函数的第一个参数是服务器的套接字描述符,第二个参数是指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数是协议地址的长度。如果accpet成功,那么它的返回值就是内核自动生成的一个全新的描述符,表示与返回客户的TCP连接。
注意:accept的第一个参数是服务器的socket描述符,在服务器开始调用socket()函数时生成,称为监控socket描述符;accept函数返回连接的套接字描述符。服务器通常只创建一个监听套接字描述符,该描述符在服务器的生存期内一直存在。内核为服务器进程接受的每个客户机连接创建一个连接套接字描述符。当服务器完成对客户机的服务时,相应的连接套接字描述符被关闭。
3.5、read()、write()等函数
一切只因东风。至此,服务器和客户之间的连接已经建立。可以调用网络I/O读写,即实现网络中不同进程之间的通信!网络I/O操作分为以下几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数。这两个函数是最通用的I/O函数。事实上,你可以用这两个函数替换上面所有其他的函数。他们的声明如下:
#包括unistd.h
ssize_t read(int fd,void *buf,size _ t count);
ssize_t write(int fd,const void *buf,size _ t count);
#包含sys/types.h
#包含sys/socket.h
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,
const struct sockaddr *dest_addr,socklen _ t addrlen);
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,
struct sockaddr *src_addr,socklen _ t * addrlen);
ssize_t sendmsg(int sockfd,const struct msghdr *msg,int flags);
ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);
read函数负责从fd中读取内容。当读取成功时,read返回实际读取的字节数。如果返回值为0,则意味着文件的结尾已被读取。如果它小于0,则意味着发生了错误。如果错误是EINTR,说明读数是中断造成的,如果是ECONNREST,说明网络连接有问题。
write函数将buf中nbytes字节的内容写入文件描述符fd。如果成功,它将返回写入的字节数。失败时返回-1,并设置errno变量。在网络程序中,当我们写入套接字文件描述符时,有两种可能。1)1)write的返回值大于0,表示已经写入了部分或全部数据。2)返回值小于0,此时出现错误。我们必须根据错误的类型来处理它。如果错误为EINTR,则意味着在写入过程中发生了中断错误。如果EPIPE表示网络连接有问题(对方已经关闭连接)。
其他的我就不一一介绍这几对I/O函数了。详情请参考man文档或百度、Google。send/recv将在下面的示例中使用。
3.6、close()函数
服务器和客户端建立连接后,会进行一些读写操作。当读写操作完成后,相应的socket描述符会被关闭,就像操作完打开的文件后调用fclose关闭打开的文件一样。
#包括unistd.h
int close(int FD);
当关闭TCP套接字的默认行为时,将套接字标记为关闭,然后立即返回到调用进程。该描述符不能再被调用进程使用,也就是说,它不能再被用作read或write的第一个参数。
注意:关闭操作只使对应的套接字描述符的引用计数为-1,只有当引用计数为0时,才会触发TCP客户端向服务器发送连接终止请求。
4、socket中TCP的三次握手建立连接详解
我们知道tcp在建立连接时要“三次握手”,也就是交换三个包。一般流程如下:
向服务器发送一个SYN J。
服务器对客户端的SYN K作出响应,并确认SYN J ACK J 1
客户端再次向服务器发送确认ACK K 1
只是三次握手,但是套接字函数中的三次握手呢?请看下图:
1.在套接字中发送的TCP三次握手
从图中可以看出,当客户端调用connect时,触发连接请求,向服务器发送SYN J包。此时connect进入阻塞状态;服务器监听连接请求时,接收SYN J包,调用accept函数接收请求,向客户端发送SYN K,ACK J 1。此时,accept进入阻塞状态;客户端收到来自服务器的SYN K,ACK J 1后,然后connect返回并确认SYN K;当服务器收到ACK K 1,accept返回,三次握手完成,连接建立。
总结:在三次握手中,第二次返回客户端的connect,第三次返回服务器的accept。
5、socket中TCP的四次握手释放连接详解
上面介绍了TCP三次握手在socket中的建立过程以及涉及到的socket函数。现在我们来介绍一下socket中四次握手释放连接的过程。请看下图:
图二。在套接字中发送的TCP四次握手
图示的过程如下:
一个应用进程先调用close主动关闭连接,然后TCP发送FINM
另一端接收到FIN M后,执行被动关机以确认此FIN。它的接收也作为文件终止符传递给应用进程,因为FIN的接收意味着应用进程不能再在相应的连接上接收额外的数据;
一段时间后,接收文件终止符的应用程序进程调用close来关闭它的套接字。这导致其TCP也发送FIN N;
接收该FIN的源发送者TCP确认它。
因此在每个方向上都有一个FIN和ACK。
6、一个例子(实践一下)
说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。
服务器端代码:
# includestdio.h
#includestdlib.h
#includestring.h
#includeerrno.h
#包含系统/类型。h
#包含sys/socket.h
#includenetinet/in.h
#定义MAXLINE 4096
int main(int argc,char** argv)
{
int listenfd,connfd
结构sockaddr _ in servaddr
char buff[4096];
int n;
if( (listenfd=socket(AF_INET,SOCK_STREAM,0))==-1 ){
printf(创建套接字错误:%s(错误号:%d)n ,strerror(错误号),错误号);
退出(0);
}
memset(servaddr,0,sizeof(serv addr));
servaddr.sin _ family=AF _ INET
服务器地址。sin _ addr。s _ addr=htonl(在addr _ ANY中);
servaddr。sin _ port=htons(6666);
if( bind(listenfd,(struct sockaddr*)servaddr,sizeof(servaddr))==-1){
printf(绑定套接字错误:%s(错误号:%d)n ,strerror(错误号),错误号);
退出(0);
}
if( listen(listenfd,10)==-1){
printf(侦听套接字错误:%s(错误号:%d)n ,strerror(错误号),错误号);
退出(0);
}
printf(======等待客户端的请求====== n’);
while(1){
if( (connfd=accept(listenfd,(struct sockaddr*)NULL,NULL))==-1){
printf(接受套接字错误:%s(错误号:%d),strerror(错误号),错误号);
继续;
}
n=recv(connfd,buff,MAXLINE,0);
buff[n]= 0 ;
printf(来自客户端的接收消息:%sn ,buff);
关闭(conn FD);
}
关闭(listenfd);
}
客户端代码:
# includestdio.h
#includestdlib.h
#includestring.h
#includeerrno.h
#包含系统/类型。h
#包含sys/socket.h
#includenetinet/in.h
#定义MAXLINE 4096
int main(int argc,char** argv)
{
int sockfd,n;
字符接收行[4096],发送行[4096];
结构sockaddr _ in servaddr
如果(argc!=2){
printf(用法:/客户端IP地址 n );
退出(0);
}
if( (sockfd=socket(AF_INET,SOCK_STREAM,0)) 0){
printf(创建套接字错误:%s(错误号:%d)n ,strerror(错误号),错误号);
退出(0);
}
memset(servaddr,0,sizeof(serv addr));
servaddr.sin _ family=AF _ INET
servaddr。sin _ port=htons(6666);
if( inet_pton(AF_INET,argv[1],servaddr.sin_addr)=0){
printf(inet_pton错误为%sn ,argv[1]);
退出(0);
}
if( connect(sockfd,(struct sockaddr*)servaddr,sizeof(servaddr)) 0){
printf(连接错误:%s(错误号:%d)n ,strerror(错误号),错误号);
退出(0);
}
printf(向服务器发送消息: n’);
fgets(发送线,4096,标准输入);
if( send(sockfd,sendline,strlen(sendline),0) 0)
{
printf(发送消息错误:%s(错误号:%d)n ,strerror(错误号),错误号);
退出(0);
}
关闭(sockfd);
退出(0);
}
当然上面的代码很简单,也有很多缺点,这就只是简单的演示(电源)插座的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要叉子()一个新的进程或者线程去处理请求等。
7、动动手
留下一个问题,欢迎大家回帖回答!是否熟悉Linux操作系统操作系统下网络编程?如熟悉,编写如下程序完成如下功能:
服务器端:
接收地址192.168.100.2的客户端信息,如信息为"客户端查询"、则打印"接收查询"
客户端:
向地址192.168.100.168的服务器端顺序发送信息"客户端查询测试","客户端查询退出",然后退出。
题目中出现的互联网协议(互联网协议的缩写)地址可以根据实际情况定。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。