linux网络编程学习

来源:互联网 发布:mac 公式编辑 编辑:程序博客网 时间:2024/04/28 13:11

什么是套接字

套接字socket是用户进程与内核网络协议栈的编程接口。
多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。
套接字主要由三部分构成:
通信的目的IP地址
使用的传输层协议(TCP/UDP)
使用的端口号
通过将这三个参数结合起来,和一个socket绑定,应用层就可以和传输层通过套接字接口区分来自不同应用程序进程和网络连接的通信,实现数据传输。

套接字描述符

linux中一切皆文件,套接字在linux系统中也是被当做文件对待,套接字描述符在linux系统中是用文件描述符实现,许多处理文件描述符的函数(read,write等)都可以处理套接字描述符。

使用socket函数创建一个套接字:
#include<sys/socket.h>int socket(int domain,int type,int protocol);

参数domain指定通信协议簇(AF_INET,AF_INET6,AF_UNIX等)
参数type指定套接字类型,可以为
SOCK_STREAM(有序可靠双向的面向连接字节流)
SOCK_DGRAM(长度固定的,无连接的不可靠的报文传递)
SOCK_RAW(IP协议的数据报接口)
参数protocol指定协议类型,一般为0,表示由给定的域和套接字类型来选择默认的协议。

调用socket函数同open函数一样,返回的是文件描述符,可以使用close函数在不需要该文件描述符时关闭对文件或者套接字的访问,同时释放该描述符以便重新使用。

当然,由于网络设备的特殊性,并不是所有的文件操作都可以应用到套接字描述符上,如lseek,因为套接字不支持文件偏移量的概念。

套接字的shutdown函数:
#include<sys/socket.h>int shutdown(int sockfd,int how);
解释一下有了close为什么还需要shutdown呢的疑惑。
第一,close操作只会在关闭了最后一个应用套接字的文件描述符之后才会释放套接字,而shutdown直接使一个套接字处于不活动状态,不管引用它的文件描述符有多少。
第二,套接字可以具体指定关闭的方向,how是SHUT_WR时会关闭写端,SHUT_RD是关闭读端,只有在how为SHUT_RDWR时才会完全关闭。

字节序的概念

字节序用来指示数据的内部字节顺序,分为大端字节序和小端字节序。大端字节序的情况下,地址高位存储值的高位,小端字节序中,地址低位存储值的低位字节序的使用没有标准可循,两种格式都有系统在使用,可以通过程序测试主机字节序:
#include<stdio.h>int main(){        union        {                int n;                char c[sizeof(int)];        } un;        un.n=0x01020304;        if(sizeof(int)==4)        {                if(un.c[0]==4&&un.c[1]==3)                        printf("little endian\n");                else if(un.c[0]==1&&un.c[1]==2)                        printf("big endian\n");        }        return 0;}

TCP/IP采用大端字节序,因此应用程序有时需要处理主机字节序和网络字节序之间的转换。
字节序转换函数:
#include<arpa/inet.h>uint32_t htonl(unint32_t hostint32);uint16_t htons(uint16_t hostinet16);uint32_t ntohl(uint32_t netinet32);uint16_t ntohs(uin16_t netint16);
四个函数完成主机字节序和网络字节序直接的互相转换,函数名中n代表网络,h代表主机,l代表长整形,s代码短整型

地址结构

地址标志了特定通信域中的套接字端点,地址用通用的的地址结构表示:
struct sockaddr {unsigned  short  sa_family;     /* address family, AF_xxx */char  sa_data[14];                 /* 14 bytes of protocol address */};
sa_family是地址家族,一般都用AF_xxx表示
sa_data是14个字节的协议地址
但是一般情况下我们使用sockaddr_in,sockaddr是给操作系统用的。

sockaddr_in区分了端口和IP地址:
struct sockaddr_in {      short            sin_family;       // 2 bytes e.g. AF_INET, AF_INET6      unsigned short   sin_port;    // 2 bytes e.g. htons(3490)      struct in_addr   sin_addr;     // 4 bytes see struct in_addr, below      char             sin_zero[8];     // 8 bytes zero this if you want to  };
使用时,将协议类型,端口,ip地址填充到sockaddr_in结构体中,然后强制转换成socketaddr作为参递给函数,例如:
struct sockaddr_in servaddr;/* initialize servaddr */bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

inet_addr和inet_ntoa函数用于套接字地址的点分十进制和长整型数之间的转换,函数inet_addr将一个点分十进制的IP转换成一个长整数型数,函数原型为:
#include<arpa/inet.h>in_addr_t inet_addr(const char *cp);
相应的,inet_ntoa将一个IP转换成一个互联网标准点分格式的字符串
<pre name="code" class="cpp">#include<arpa/inet.h>char *inet_ntoa (struct in_addr);


套接字和地址的绑定

进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。许多时候内核会我们自动绑定一个地址,然而有时用户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由bind的函数完成。bind的函数原型为:

#include<sys/socket.h> int bind( int sockfd, struct sockaddr* addr, socklen_t addrlen);


参数sockfd指定地址与哪个套接字绑定,这是一个由之前的socket函数调用返回的套接字。调用bind的函数之后,该套接字与一个相应的地址关联,发送到这个地址的数据可以通过这个套接字来读取与使用。
参数addr指定地址,这是一个已经经过填写的有效的地址结构。调用bind之后这个地址与参数sockfd指定的套接字关联。
参数addrlen,为addr结构的大小,内核在复制或传递地址给驱动的时候,需要根据这个值来确定需要复制多少数据。
bind函数只在用户进程想与一个具体的地址或者端口相关联的的时候才需要被调用,如果应用程序没有这个需要,可以不调用bind函数,程序可以依赖内核的自动的选址机制来完成自动地址选择,而不需要调用bind的函数,同时也避免不必要的复杂度。
一般情况下,服务器进程需要调用 bind函数,对于客户进程则不需要调用bind函数。
在使用bind函数时,要保证指定的地址和创建套接字时指定的domain支持的格式相匹配,同时要保证端口号不小于1024,除非该进程具有相应的特权。

连接的建立

对于面向连接的网络服务(SOCK_STREAM),在开始交换数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接,connect函数可以实现这个功能:

#include<sys/socket.h>int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
参数sockfd指定数据发送的套接字,内核需要维护大量IO通道,所以用户必需通过这个参数告诉内核从哪个IO通道,此处就是从哪个socket接口中发送数据,sockfd是先前socket返回的文件描述符。
参数server_addr指定数据发送的目的地,也就是服务器端的地址。
参数addrlen指定server_addr结构体的长度。我们知道系统中存在大量的地址结构,但socket接口只是通过一个统一的结构来指定参数类型,所以需要指定一个长度,以使内核在进行参数复制的时候有个有个界限。

connect函数在连接失败时,会返回错误值,应用程序必须能够处理connect返回的错误。
客户端使用connect发起连接,要能够成功的建立连接,服务器端必须使用listen函数来监听套接字,此时服务器可以接受连接。
函数调用形式为:
#include <sys/socket.h>int listen(int sockfd, int backlog);
listen函数一般在 调用socket和bind函数后使用。
sockfd为socket函数返回的并通过bind函数绑定了sockaddr地址的套接字描述符
backlog参数指定同时能处理的最大连接要求。
一旦服务器调用了listen,套接字就能接收到连接请求,使用函数accept获得连接请求并建立连接。
#include<sys/socket.h>int accept(int sockfd, struct sockaddr* addr, socklen_t* len)
函数accept所返回的文件描述符时套接字描述符,该描述符连接到调用connect的客户端
而传给函数的accept的原始套接字sockfd并没有关联到这个连接,而是继续保持可用状态并接受其他连接请求。
在没有连接请求的情况下,调用accept函数会阻塞直到一个请求到来。

数据传输

send函数用来将数据由指定的socket传给对方主机,函数原型为:
<pre name="code" class="cpp"> #include <sys/socket.h>int send(int s, const void * msg, int len, unsigned int falgs);
参数s 为已建立好连接的socket. 
参数msg 指向欲连线的数据内容
参数len 则为数据长度. 参数flags 一般设0,其他数值定义如下:
  MSG_OOB 传送的数据以out-of-band 送出.  
  MSG_DONTROUTE 取消路由表查询   
  MSG_DONTWAIT 设置为不可阻断运作   
 MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断.
使用send时,套接字必须已经连接,因此在使用send发送UDP数据报时,必须事先以UDP套接字为参数调用connect( )函数。如果使用sendto( )函数的话,则可以在发送数据时再指定目标地址及端口。
sendto函数:
#include < sys/socket.h >int sendto ( socket s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen ) ;
sendto函数和send函数类似,但是sendto函数允许在无连接的套接字上指定一个目标地址。
和send,sendto函数对应的是recv,recvfrom函数,分别用于从已经建立连接的套接字描述符和无连接的套接字描述符中接收数据。
recv的函数原型:
#include<sys/socket.h>int recv( SOCKET s,   char FAR *buf,    int len,   int flags   );  
第一个参数指定接收端套接字描述符
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
第三个参数指明buf的长度
第四个参数一般取0,也可以取下面的值,表示特殊的含义:
MSG_OOB 发送或接收带外数据。
MSG_PEEK 窥看外来消息。
MSG_WAITALL 等待所有数据。

recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
使用recv函数必须事先调用connect函数,而recvfrom函数可以在接收数据报时获取源IP地址和端口,通常用于无连接套接字。
recvfrom函数原型:
<pre name="code" class="cpp">#include<sys/socket.h>int recvfrom(SOCKET s,void *buf,int len,unsigned int flags, struct sockaddr *from,int *fromlen);

如果from参数非空,recvfrom函数将会在from指定的地址填充发送者的套接字地址,fromlen函数用来指定from缓冲区的大小。
下面分别给出UDP和TCP的例子,综合应用上面给出的各个阶段的函数来实现UDP和TCP的功能。
UDP客户端代码:
#include<sys/types.h>#include <sys/types.h>#include <sys/socket.h>#include <linux/in.h>#include <string.h>#include <stdio.h>#include <stdlib.h>#define PORT 8888int main(int argc,char* argv[]){int s;struct sockaddr_in server_addr,client_addr;  s=socket(AF_INET,SOCK_DGRAM,0);//建立数据报套接字 if(s<0){    perror("socket error");    return;}memset(&server_addr,0,sizeof(server_addr));//将地址结构清0server_addr.sin_family=AF_INET;server_addr.sin_port=htons(PORT);server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//首先向服务器发送数据char buffer[1024];int size;int len=sizeof(server_addr);bzero(buffer,1024);for(;;){   size=read(0,buffer,1024);//从标准输入读到的数据   if(size>0){//向服务器发送    struct sockaddr_in from;    sendto(s,buffer,size,0,(struct sockaddr*)&server_addr,len);//第3个参数是目标地址,size表示发送的数据长度     bzero(buffer,1024);    int recvsize=recvfrom(s,buffer,1024,0,(struct sockaddr*)&from,&len);//从服务器接收数据,from表示服务器端的地址信息    //struct sockaddr* from代表服务器端的信息,打印服务器端的IP地址与端口号    char *ip=(char*)inet_ntoa(from.sin_addr);    printf("Server IP:%s\n",ip);    short port=ntohs(from.sin_port);    printf("Server Port:%d\n",port);    if(recvsize>0){        printf("recevied:%s",buffer);    }}}}
UDP服务器端代码:
#include<sys/types.h>#include<sys/socket.h>#include<linux/in.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#define PORT 8888int main(int argc,char* argv[]){int socket_fd;//套接字描述符//服务器地址结构和收到的客户端地址结构struct sockaddr_in server_addr,client_addr; int len;char buffer[1024];socket_fd=socket(AF_INET,SOCK_DGRAM,0);//建立一个数据包套接字//参数二代表socket类型//参数三代表协议,0代表选择参数二对应的默认协议if(socket_fd<0)//AF_INET代表TCP/IP协议簇{perror("socket error");return;}memset(&server_addr,0,sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_port=htons(PORT);//将主机字节序转化为网络字节序,网络字节序采用的是big-endian的排序方式server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//将无符号长整型主机字节序转化为网络字节序int ret=bind(socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));//将IP地址和端口号绑定到套接字上,表示服务器用此端口进行数据的接受和发送if(ret<0){perror("bind error");return;}for(;;){bzero(buffer,1024);//参数1 要置零的起始地址 参数2 要置零的长度len=sizeof(client_addr);int rec=recvfrom(socket_fd,buffer,1024,0,(struct sockaddr*)&client_addr,&len);//返回接收到的字符数printf("%d\n",rec);printf("server:%s\n",buffer);if(ret==-1){perror("recv error");return;}sendto(socket_fd,buffer,rec,0,(struct sockaddr*)&client_addr,len);//sockaddr和sockaddr_in类似char* addr;addr=(char*)inet_ntoa(client_addr.sin_addr);printf("client IP is:%s\n",addr);int port=ntohs(client_addr.sin_port);  //输出客户端所用的端口号printf("Port is %d\n",port);}close(socket_fd);return 0;}








0 0