socket简介

Socket接口是TCP/IP网络的APISocket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的 Socket数据传输是一种特殊的I/OSocket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返 回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式SocketSOCK_STREAM)和数据报式SocketSOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据 报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。


套接字描述符

其实就是一个整数,我们最熟悉的句柄是012三个,0是标准输入,1是标准输出,2是标准错误输出。012是整数表示的,对应的FILE *结构的表示就是stdinstdoutstderr
套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。特别是,当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。
在许多操作系统中,套接字描述符和其他I/O描述符是集成在一起的,所以应用程序可以对文件进行套接字I/OI/O读/写操作。
当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。
对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。


socket()描述字建立函数

建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。socket函数原型为:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数

  • domain:
    指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);
    • AF_INET IPv4因特网域
    • AF_INET6 IPv6因特网域
    • AF_UNIX Unix
    • AF_ROUTE 路由套接字
    • AF_KEY 密钥套接字
    • AF_UNSPEC 未指定
  • type参数指定socket的类型:
    • SOCK_STREAM
      流式套接字提供可靠的、面向连接的通信流;它使用TCP 协议,从而保证了数据传输的正确性和顺序性
    • SOCK_DGRAM
      数据报套接字定义了一种无连接的服 ,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP
    • SOCK_RAW
      允许程序使用低层协议,原始套接字允许对底层协议如IPICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。
  • protocol
    通常赋值”0“。
    • 0 选择type类型对应的默认协议
    • IPPROTO_TCP TCP传输协议
    • IPPROTO_UDP UDP传输协议
    • IPPROTO_SCTP SCTP传输协议
    • IPPROTO_TIPC TIPC传输协议

返回值

  • 返回一个整型socket描述符,可以在后面的调用中使用它。socket描述符是一个整数,但它指向内部数据结构(socket结构)的指针,它指向描述符表入口。调用socket函数时,socket执行体将建立一个socket结构,实际上”建立一个socket“意味着为一个socket数据结构分配存储空间。socket执行体为你管理描述符表。该空间还有许多成员,并不会在初始化时被填充,可调用其它函数进行填充完善。
      两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。socket数据结构中包含这五种信息.

bind()函数:IP号端口号与相应描述字赋值函数

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

功能

  • 用于绑定IP地址和端口号到socketfd

参数

  • sockfd
    是一个socket描述符
  • addr
    是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同
//ipv4对应的是: 
struct sockaddr{
                unisgned short  as_family;    // 协议族
                char            sa_data[14];  // IP+端口
       };
同等替换:
struct sockaddr_in {
    sa_family_t    sin_family; /* 协议族  */
    in_port_t      sin_port;   /* 端口号*/
    struct in_addr sin_addr;   /*IP地址结构体*/
    unsigned char  sin_zero[8];  /* 填充   没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换*/
};
/*两个结构是等同的可以先互转换,第一个结构将地址和端口绑定了,第二个结构将两者分开表示*/
/* IP地址结构如下:为32位字 */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};

//ipv6对应的是: 
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};
struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};

//Unix域对应的是: 
#define UNIX_PATH_MAX    108

struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

addrlen

  • 对应的是地址的长度,常被设置为sizeof(struct sockaddr)

返回值

  • bind()函数在成功被调用时返回0;遇到错误时返回-1并将errno置为相应的错误号

对于 TCP 协议,调用 bind 函数可以指定一个端口号,或指定一个 IP 地址,也可以两者都指定,还可以都不指定。若 TCP 客户端或服务器端不调用bind 函数绑定一个端口号,当调用connectlisten 函数时,内核会为相应的套接字选择一个临时端口号。一般TCP 客户端使用内核为其选择一个临时的端口号,而服务器端通过调用bind 函数将端口号与相应的套接字绑定。进程可以把一个特定的 IP 地址捆绑到它的套接字上,但是这个 IP 地址必须属于其所在主机的网络接口之一。对于 TCP 客户端,这就为在套接字上发送的 IP 数据报指派了源 IP 地址。对于 TCP服务器端,这就限定该套接字只接收那些目的地为这个IP 地址的客户端连接。TCP 客户端一般不把IP 地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源IP 地址,而所用外出接口则取决于到达服务器端所需的路径。若TCP 服务器端没有把 IP地址捆绑到它的套接字上,内核就把客户端发送的SYN的目的IP 地址作为服务器端的源IP 地址。


connect()函数:客户机连接主机

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

功能

  • 该函数用于绑定之后的client端(客户端),与服务器建立连接

参数

  • sockfd
    是目的服务器的sockect描述符
  • addr
    是服务器端的IP地址和端口号的地址结构指针
  • addrlen
    地址长度常被设置为sizeof(struct sockaddr)

返回值

  • 成功返回0,遇到错误时返回-1,并且errno中包含相应的错误码

listen()函数:监听设置函数

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

功能

  • 设置能处理的最大连接数,listen()并未开始接受连线,只是设置sockectlisten模式 ,listen函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
  • 内核为任何一个给定监听套接字维护两个队列:
    • 未完成连接队列,每个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_REVD状态;
    • 已完成连接队列,每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED 状态;

参数

  • sockfd
    sockfdsocket系统调用返回的服务器端socket描述符
  • backlog
    backlog指定在请求队列中允许的最大请求数

accept()函数

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

功能

  • accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。

参数

  • sockfd
    sockfdsocket系统调用返回的服务器端socket描述符
  • addr
    用来返回已连接的对端(客户端)的协议地址
  • addrled
    客户端地址长度

返回值

  • 该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。

shutdown()

#include <sys/socket.h>   
int shutdown(int sockfd, int howto);   

功能

  • 关闭套接字

参数

  • sockfd
    sockfdsocket系统调用返回的服务器端socket描述符
  • howto
    • 1.SHUT_RD:值为0,关闭连接的读这一半;
    • 2.SHUT_WR:值为1,关闭连接的写这一半;
    • 3.SHUT_RDWR:值为2,连接的读和写都关闭。

close()

#include <unistd.h>   
int close(int sockfd);  //彻底关闭套接字

getsockname ,getpeername 函数

/*  
     * 函数功能:获取已绑定到一个套接字的地址;  
     * 返回值:若成功则返回0,若出错则返回-1;  
     * 函数原型:  
     */   
    #include <sys/socket.h>   
    int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);   
    /*  
     * 说明:  
     * 调用该函数之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小;  
     * 返回时,该整数会被设置成返回地址的大小,如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错;  
     */   

    /*  
     * 函数功能:获取套接字对方连接的地址;  
     * 返回值:若成功则返回0,若出错则返回-1;  
     * 函数原型:  
     */   
    #include <sys/socket.h>   
    int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);   
    /*  
     * 说明:  
     * 该函数除了返回对方的地址之外,其他功能和getsockname一样;  
     */   

当不调用bind或调用bind没有指定本地IP地址和端口时,可以调用函数getsockname来返回内核分配的本地IP地址和端口号,还可以获取到套接口的协议簇。当一个新的连接建立时,服务器端也可以调用getsockname来获取分配给此连接的本地IP地址;当服务器端的一个子进程调用函数exec启动执行时,只能调用getpeername来获取客户端IP地址和端口号。


套接字通信流程

大多数TCP服务器是并发的,它们为每一个待处理的客户连接调用fork派生一个子进程,当一个连接建立时,accept返回,服务器调用fork函数新建一个子进程,由子进程处理与客户的连接,父进程则可以在监听套接字上再次调用accept来处理下一个客户的连接。
所有的客户端和服务器都是从调用socket开始的,它返回一个描述符,客户端调用connect,服务器调用bindlistenaccept。建立连接之后I/O函数进行数据的传递。通信结束一般用close关闭套接字。

补充知识

套接口即网络进程的ID;网络通信归根到底即为进程间的通信;套接字中包含了端口号,用来确定进程,一个端口号一次只能分配给一个进程,即端口号与进程是一一对应的;

辅助函数

1、字节排序函数

#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue);    //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue);    //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue);     //返回主机字节序的值
uint32_t ntohl(uint32_t net32bitvalue);     //返回主机字节序的值

这几个函数主要的作用就是讲主机字节序的地址转化为网络字节序,什么意思呢?计算机处理器型号的不同,数据存储方式有所不同,偷得采用大端存储,有的采用小端存储,例如:同样存储0x1234,大小端机器存储顺序是相反的,但网络IP地址是统一规定的,这些函数就是将机器大小端存储造成的差异取消掉,使之将他们变为标准IP,这些函数中,h代表hostn代表nets代表short(两个字节),l代表long4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANYINADDR_ANY指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。

2、字节操作函数

  字节操作函数主要是用于操作结构体中的某几个字节,在处理套接字结构中有IP地址等字段,包含0字节却不是C字符串,需要特定的函数来进行处理。不能直接赋0

#include <string.h>
void bzero(void *dest,size_t nbytes);
void bcopy(const void *src,void *dest,size_t nbytes);
int bcmp(const void *prt1,const void *ptr2,size_t nbytes);  //相等返回0,否则非0

void *memset(void *dest,int c,size_t len);
void *memcpy(void *dest,const void *src,size_t nbytes);
int memcmp(const void *ptr1,const void *ptr2,size_t nbytes); //相等返回0,否则非0

  以b打头的函数为支持套接口函数的系统所提供,mem为支持ANSI C库提供的函数;其中,bzero将指定数目的字节设置为0bcopy将指定数目的字节从源字节串复制到目的字节串。bcmp比较两个字节串,相同返回0memset将目标中指定数据的字节设置为指定的值(不一定是0),memcpy为复制,memcmp为比较,两者与前面类似。

3、地址转换函数

  一般使用ASCII字符串表示IP地址,也就是点分十进制数表示(218.170.19.1),但在套接字中使用32位网络字节序,保存的是二进制值,因此必须进行地址转换。

#include <arpa/inet.h>
in_addr_t inet_addr(const char *straddr);    //字符串有效返回网络字节序的IP地址,否则INADDR_NONE
int inet_aton(const char* straddr,struct in_addr *addrp);//字符串有效返回1,否则0
char* inet_ntoa(struct in_addr inaddr); //返回一个字符串指针

int inet_pton(int family, const char* str, void* addr);
//成功返回1,字符串无效返回0,出错返回-1
const char* inet_ntop(int family, const void* addr, char* str, size_t len);
//成功返回结果指针,出错返回NULL

/*说明
 *后面两个函数为新函数,支持IPv4和IPv6,family用来指定:AF_INET,AF_INET6
 *inet_ntop函数len为目标存储单元大小,str指针就是函数返回值
 */

inet_aton将一个字符串转换为32位网络字节序二进制值,用结构in_addr存储。inet_addr功能相同(废弃,不使用),inet_ntoa进行相反的操作。
  

4、字节流读取函数

  在套接字通信中进行字节读取函数:read(),write()。与I/O中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少。

ssize_t write(int fd, const void*buf,size_t nbytes);
ssize_t read(int fd,void *buf,size_t nbyte); 

/*说明
 *函数均返回读或写的字节个数,出错则返回-1
 */

第一个将buf中的nbytes个字节写入到文件描述符fd中,成功时返回写的字节数。第二个为从fd中读取nbyte个字节到buf中,返回实际所读的字节数。详细应用说明参考使用read write 读写socket(套节字) 。
网络I/O还有一些函数,例如:recv()/send()readv()/writev()recvmsg()/sendmsg()recvfrom()/sendto()等

5、在TCP套接字上发送数据函数:有连接

ssize_t send(int s,const void *msg,size_t len,int flags);
//包含3要素:套接字s,待发数据msg,数据长度len
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0

6、在TCP套接字上接收数据函数:有连接

ssize_t recv(int s,void *buf,size_t len,int flags);
//包含3要素:套接字s,接收缓冲区buf,长度len
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0

7、在UCP套接字上发送数据函数:无连接

ssize_t sendto(int s,const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
//函数功能与函数send类似,但函数sendto不需要套接字处于连接状态,所以
//该函数通常用来发送UDP数据,同时因为是无连接的套接字,在使用sendto时
//需要指定数据的目的地址,参数msg指向待发送数据的缓冲区。
//参数len指定了待发送数据的长度
//参数flags是控制选项,含义与send函数中的一致
//参数to用于指定目的地址,目的地址的长度由tolen指定

8、在UDP套接字上接收数据函数:无连接

ssize_t recvfrom(int s ,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
//与函数recv功能类似,只是函数recv只能用于面向连接的套接字,而函数
//recvfrom没有此限制,可以用于从无连接的套接字上接收数据
//参数buf指向接收缓冲区
//参数len指定了缓冲区的大小
//参数flags是控制选项,含义与recv中的一致
//如果参数from非空,且该套接字不是面向连接的,则函数recvfrom返回时,
//参数from中将保存数据的源地址
//参数fromlen在调用recvfrom前为参数from的长度,调用recvfrom后将
//保存from的实际大小

网络编程示例,写一个简单的QQ程序

服务器端

#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdio.h>
#include<time.h>
#define port 8888
char* getDateTime();
int main()
{
    //定义服务器 客户端的套接字描述符
    int sfd=0,cfd=0,ret=0,addrlen=0,count=0;

    char * nowtime;
    //定义接收缓存区
    unsigned char ReadBuffer[1024];
    unsigned char SendBuffer[1024];
    //定义服务器 客户端的套接字数据结构
    struct sockaddr_in saddr,caddr;

    //数据结构清0
    memset(&saddr,0,sizeof(struct sockaddr));
    memset(&caddr,0,sizeof(struct sockaddr));

    //创建服务器套接字
    sfd=socket(AF_INET,SOCK_STREAM,0);

    //判断
    if(sfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    //初始化服务器套接字数据结构
    saddr.sin_family = AF_INET;
    saddr.sin_port  = htons(port);
    saddr.sin_addr.s_addr = INADDR_ANY;
    bzero(saddr.sin_zero,8);
    //套接字绑定
    ret = bind(sfd,(struct sockaddr*)(&saddr),sizeof(struct sockaddr));
    if(ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    //监听设置
    ret=listen(sfd,10);
    if(ret == -1)
    {
        perror("listen");
        exit(-1);
    }
    while(1)
    {
        //接收等待
        addrlen = sizeof(struct sockaddr);
        cfd=accept(sfd,(struct sockaddr*)(&caddr),&addrlen);
        if(cfd == -1)
        {
            perror("accept");
        }
        printf("从机ip是%s\n",inet_ntoa(caddr.sin_addr));
        if(fork()==0)
        {
            while(1)
            {
                if(fork()==0)
                {
                    fgets(SendBuffer,1024,stdin);
                    send(cfd,SendBuffer,1024,0);
                    printf("\n");
                }
               nowtime=getDateTime();
               count= recv(cfd,ReadBuffer,1024,0);
               //printf("count is %d\n",count);
               ReadBuffer[count]='\0';
               printf("李京京 %s\n",nowtime);
               printf("%s\n",ReadBuffer);
            }
        }
    }

}


char* getDateTime()
{
    static char nowtime[20];
    time_t rawtime;
    struct tm* ltime;
    time(&rawtime);
    ltime=localtime(&rawtime);
    strftime(nowtime,20,"%Y-%m-%d %H:%M:%S",ltime);
    return nowtime;
}

客户端

#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdio.h>
#include<time.h>
#define port 8888
char* getDateTime();
int main(int argc,char **argv)
{
    //定义服务器 客户端的套接字描述符
    int cfd=0,ret=0;

    char * nowtime;
    //定义接收缓存区
    unsigned char SendBuffer[1024];
    unsigned char ReadBuffer[1024];
    //定义服务器 客户端的套接字数据结构
    struct sockaddr_in saddr,caddr;

    //数据结构清0
    memset(&saddr,0,sizeof(struct sockaddr));
    memset(&caddr,0,sizeof(struct sockaddr));

    //创建服务器套接字
    cfd=socket(AF_INET,SOCK_STREAM,0);

    //判断
    if(cfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    //初始化服务器套接字数据结构
    caddr.sin_family = AF_INET;
    caddr.sin_port  = htons(port);
    inet_aton(argv[1],&caddr.sin_addr);
    bzero(caddr.sin_zero,8);
    ret = connect(cfd,(struct sockaddr*)(&caddr),sizeof(struct sockaddr));
    if(ret == -1)
    {
        perror("ret");
        exit(-1);
    }
    while(1)
    {
        if(fork()==0)
        {
            nowtime=getDateTime();
            ret=recv(cfd,ReadBuffer,1024,0);
            printf("张超 %s\n",nowtime);
            ReadBuffer[ret]='\0';
            printf("%s\n",ReadBuffer);
        }
        fgets(SendBuffer,1024,stdin);
        send(cfd,SendBuffer,1024,0);
        printf("\n");
    }
}

char* getDateTime()
{
    static char nowtime[20];
    time_t rawtime;
    struct tm* ltime;
    time(&rawtime);
    ltime=localtime(&rawtime);
    strftime(nowtime,20,"%Y-%m-%d %H:%M:%S",ltime);
    return nowtime;
}
GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐