linux网络编程socket,linux的socket命令
Linux套接字编程(不限于Linux)-吴沁-博客公园
Linux套接字编程(不限于Linux)
2010-12-12 21:58
经过
秦,
28238
读书,
25
评论,
收藏,
编辑
“什么都是插座!”
有点夸张,但事实是现在的网络编程几乎都在使用socket。
3354关于实用编程和开源项目研究的思考。
我们非常清楚信息交流的价值。进程如何在网络中通信?比如我们每天打开浏览器浏览网页时,浏览器进程是如何与web服务器进行通信的?用QQ聊天时,QQ进程如何与服务器或好友所在的QQ进程进行通信?这一切都靠插座?什么是插座?插座有哪些类型?还有socket的基本功能,本文要介绍的。本文的主要内容如下:
1.进程如何在网络中通信?2.插座是什么?3.socket的基本操作3.1,socket()函数3.2,bind()函数3.3,listen(),connect()函数3.4,accept()函数3.5,read(),write()函数3.6,close()函数4,TCP in socket的三次握手连接5,TCP in socket。1.进程如何在网络中通信?本地进程间通信(IPC)有多种方式,但可以归纳为以下四类:
消息传递(管道、FIFO、消息队列)同步(互斥、条件变量、读写锁、文件和写记录锁、信号量)共享内存(匿名和命名)远程过程调用(Solaris gate和Sun RPC)但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信。首先要解决的问题是如何唯一标识一个进程,否则通信将无法进行!进程可以由进程PID在本地唯一标识,但在网络中不可行。事实上,TCP/ip协议家族已经帮助我们解决了这个问题。网络层的IP地址可以唯一标识网络中的主机,而传输层的协议端口可以唯一标识主机中的应用(进程)。这样,三元组(ip地址、协议、端口)可以用来标识网络中的进程,网络中的进程通信可以使用这个标志与其他进程进行交互。
使用TCP/IP协议的应用通常使用API:UNIX BSD的socket和UNIX System V(已淘汰)的TLI来实现网络进程间的通信。目前几乎所有的应用都采用socket,现在是互联网时代,进程通信无处不在,这也是我说“一切都是socket”的原因。
2.什么是插座?我们已经知道网络中的进程通过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()来监听这个socket。如果客户端调用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()/send to()我推荐使用recvmsg()/send msg()函数,这是最通用的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.套接字中tcp的三次握手连接。详解:我们知道TCP连接需要“三次握手”,即交换三个数据包。一般流程如下:
向服务器发送一个SYN J。服务器对客户端的SYN K作出响应,并确认SYN J ACK J 1。客户端向服务器发送确认ACK K 1。都结束了,三次握手。但是这种三次握手发生在socket的哪些函数中呢?请看下图:
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.套接字中TCP的四次握手以释放连接。以上介绍了TCP三次握手在socket中的建立过程以及涉及到的socket函数。现在我们来介绍一下socket中四次握手释放连接的过程。请看下图:
图二。在套接字中发送的TCP四次握手
图示的过程如下:
某个应用进程首先调用关闭主动关闭连接,这时三氯苯酚发送一个鳍m;另一端接收到鳍M之后,执行被动关闭,对这个鳍状物进行确认。它的接收也作为文件结束符传递给应用进程,因为鳍状物的接收意味着应用进程在相应的连接上再也接收不到额外数据;一段时间之后,接收到文件结束符的应用进程调用关闭关闭它的插座。这导致它的三氯苯酚也发送一个鳍n;接收到这个鳍状物的源发送端三氯苯酚对它进行确认。这样每个方向上都有一个鳍状物和ACK。
6、一个例子(实践一下)说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。
服务器端代码:
服务器端
#包含标准视频
#包含标准库
#包含字符串。h
#包含错误号h
#包含sys/types.h
#包含sys/socket.h
#包含netinet/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);
客户端代码:
客户端
#包含标准视频
#包含标准库
#包含字符串。h
#包含错误号h
#包含sys/types.h
#包含sys/socket.h
#包含netinet/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的服务器发送“客户端查询测试”、“Cleint查询”、“客户端查询退出”信息,然后退出。
题目中出现的ip地址可以根据实际情况确定。
3354本文只介绍简单的套接字编程。
更复杂的需要自己去深化。
(unix域socket)用udp=128K发送消息会报错ENOBUFS(实际socket编程遇到的问题,希望对你有帮助)