Socket

套接字Socket

套接字用于两个进程之间互相通信,与其他所有进程间通信方式不同的是,这两个进程可以分别位于两台不同的用网络连接的计算机。同时,Socket也可以用来连接同一台计算机上的两个进程。

套接字使用流程

首先,服务器应用会使用Socket()创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,不能与其他进程共享。接下来,服务器进程会给套接字起个名字。本地套接字的名字是Linux文件系统中的文件名,一般放在/tmp/usr/tmp中。对于网络套接字,它的名字是与客户端连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。例如,Web服务器一般在80端口上创建一个套接字,这是一个专用的标识符。Web浏览器知道对于用户想要访问的Web站点,应该使用端口80来建立HTTP连接。我们用系统调用bind()来给套接字命名。然后服务器进程就开始等待客户连接到这个命名套接字。系统调用listen()的作用是,创建一个队列用来存放来自客户的连接请求,服务器通过系统调用accept()来接受客户的连接。服务器调用accept()时,它会创建一个新的命名套接字,这个新的套接字用来专门跟这个客户进程通信,而原来的命名套接字继续用来处理其它客户的连接请求。

对于客户端,首先调用socket()创建一个为命名的套接字,接着将服务器的命名套接字作为地址来调用connect()与服务器连接。连接建立完成之后,只需要将套接字当成文件描述符使用即可。下面是使用流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TJlJx2in-1615542012992)(Socket.png)]

套接字基本函数

上面的套接字基本流程中使用到的函数如下:

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
int bind(int socket, const struct sockaddr *address, size_t address_len);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t *address_len);
int connect(int socket, const struct sockaddr *address, size_t address_len);

下面是这些函数的介绍。

创建套接字socket()

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

这个系统调用创建一个套接字并返回一个描述符,之后就可以用这个描述符来访问套接字。其中domain表示套接字的域,type表示套接字的通信类型,protocol指定使用的协议。

domain套接字的域指的是套接字的通信媒介,例如使用英特网通信的网络套接字的域为AF_INET,使用UNIX文件系统通信的本地套接字的域为AF_UNIX。不同域的套接字对应可以使用不同的通信方式和通信协议。domain参数可以指定的域如下:

说明
AF_UNIXUNIX域协议(文件系统套接字)
AF_INETARPA英特网协议(UNIX网络套接字)
AF_ISOISO标准协议
AF_NS施乐(Xerox)网络系统协议
AF_IPXNovell IPX网络系统协议
AF_APPLETALKAppletalk DDS

其中最常用的两个域是AF_UNIXAF_INET

type参数用来指定套接字的通信特性,它可能的取值有SOCK_STREAM(流形式)和SOCK_DGRAM(数据报形式)。流套接字会将数据以流的形式传输,它会保证数据的正确性,并且会保持通信双方的连接。而数据报形式只是将数据作为一个消息发送出去,而不管数据是否正确到达,也不会建立通信双方的连接。因为流类型需要保证数据的正确性,并且要保持通信双方的连接,所以所需的资源会更多,数据报形式则相反。这两者的区别类似英特网中TCPUDP之间的差别,事实上在AF_INET域中,这两种不同的通信类型也确实是用这两种协议实现的。

protocol用来指定通信使用的协议,一般domaintype确定了之后,protocol就不需要指定了,因为前两个参数就已经可以确定通信要使用的协议了,只需要将protocol设置成0(默认协议)即可。

socket()返回一个描述符,这个描述符的使用类似文件描述符,当这个套接字连接到另一端的套接字之后,我们就可以使用write()read()系统调用来传输数据了。

命名套接字bind()

#include <sys/socket.h>

int bind(int socket, const struct sockaddr *address, size_t address_len);

这个函数用来将套接字命名,也就是将套接字与地址绑定,绑定完成之后,就可以通过地址进行套接字之间的通信。socketsocket()返回的描述符。

address参数表示要与套接字绑定的地址,这个地址是一个结构体,而这个结构体的定义随着套接字域的不同而不同,例如在AF_UNIX域中的定义为:

#include <sys/un.h>

struct sockaddr_un{
    sa_family_t sun_family;
    char        sun_path[];
};

而在AF_INET中的定义为:

#include <netinet/in.h>

struct sockaddr_in{
    short int       sin_family;
    unsigned short  int sin_port;
    struct in_addr  sin_addr;
};

struct in_addr{
    unsigned long int s_addr;
};

所有的地址结构体都会以一个指明地址类型的变量作为第一个成员,例如sockaddr_un中的sun_familysockaddr_in中的sim_family,结构体剩下的部分将会用来表示地址。因为不同的域有不同类型的地址结构体,所以在使用这个地址调用的时候,需要用强制类型转换将各种类型的地址结构体转换成同一种类型struct sockaddr*

很多时候一台服务器上面会有多张网卡,也就会有多个IP地址,这时就可以将sockaddr_in.sin_addr.s_addr设置成INADDR_ANY,这样就可以保证这台服务器中该端口接受到的所有请求连接都由这个套接字处理。

address_len参数用来指定地址的长度,因为不同的域有着不同的地址格式,长度也不同,所以需要用这个参数来告诉系统地址的长度是多少。

这个函数在执行成功时返回0,失败时-1并设置错误代码。

创建套接字队列listen()

#include <sys/socket.h>

int listen(int socket, int backlog);

为了能够接受连接请求,需要一个队列来存储这些连接请求,listen()就是用来实现这一功能的。socket参数是socket()返回的描述符,backlog参数为队列的最大长度,如果队列已满,那么后面传来的连接请求将会被拒绝。

这个函数在执行成功时返回0,失败时返回-1并设置错误代码。

接受连接accept()

#include <sys/socket.h>

int accept(int socket, struct sockaddr *address, size_t *address_len);

这个函数用来取出socket队列中的第一个请求并接受他的请求,如果当前的请求队列为空,则函数会阻塞,函数执行成功之后会返回一个新的socket描述符,这个socket描述符的类型与监听描述符相同。

accept()接受请求的同时,还会将客户端的地址存入第二个参数address指向的内存中,如果我们不需要这个地址,可以将这个参数设置为空指针。因为不同的域有着不同的地址长度,所以第三个参数address_len用来指定地址长度,超出这个长度的内容将会被截断。

可以使用fcntl()来对描述符设置O_NONBLOCK标志来让accept()函数在即使请求队列为空的时候也立即返回,如下所示:

int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, O_NONBLOCK | flags);

这个函数在执行成功时返回一个新的描述符,失败时返回-1并设置错误代码。

请求连接connect()

#include <sys/socket.h>

int connect(int socket, const struct sockaddr *address, size_t address_len);

客户端通过在本地用一个未命名的套接字与服务器端的套接字建立连接来进行通信,connect()就是用来实现这一功能的函数。如果连接不能建立,connect()将阻塞一段超时时间,如果在这一段超时时间结束之前连接还不能建立,则调用失败。但如果connect()被一个信号中断,即使这个信号得到处理,调用还是会失败,但还是会继续以异步的方式尝试连接,程序必须在之后用select()检查连接是否成功建立。

accept()一样,可以通过设置O_NONBLOCK标志来使这个函数不阻塞。此时,如果连接不能立刻建立,connect()将失败并把errno设置成EINPROGRESS,而连接将以异步的方式继续进行。

关闭套接字

跟文件描述符一样,套接字使用完毕之后,可以用close()来关闭套接字。

例子

下面是一个两个程序通过计算机的本地回路网络进行通讯的例子,在选择端口号的时候,要选择大于1024的端口号,因为小于1024的端口号是留给系统使用的。

这个例子的服务器会一直循环等待客户发送请求,并把接收到的请求处理完之后发送回客户端,而客户端发送完数据等着接受处理好的数据即可。从下面的例子可以看出一个socket连接可以双向传输数据accept()会返回一个专用的端口,完成通信之后需要将这个专用端口关闭

=========================server.c========================
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void){
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;

    //创建一个监听套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //为监听套接字命名
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr("127.0.0.1");    //转换成网络字节序
    server_address.sin_port = 9734;                             //不需要转换成网络字节序
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

    //创建一个连接队列,开始等待客户进程
    listen(server_sockfd, 5);
    while(1){
        char ch;

        printf("server waiting\n");
        client_len = sizeof(client_address);
        client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_address, &client_len);
        read(client_sockfd, &ch, 1);
        ch++;
        write(client_sockfd, &ch, 1);
        close(client_sockfd);
    }
}
=========================================================
=========================client.c========================
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void){
    int sockfd;
    int len;
    struct sockaddr_in address;
    int result;
    char ch = 'A';

    //创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //设置要连接的服务器套接字
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");   //转换成网络字节序
    address.sin_port = htons(9734);                     //转换成网络字节序
    len = sizeof(address);

    //向服务器发送连接请求
    result = connect(sockfd, (struct sockaddr *)&address, len);
    if(result == -1){
        perror("failed to connect\n");
        exit(1);
    }

    write(sockfd, &ch, 1);
    read(sockfd, &ch, 1);
    printf("char from server = %c\n", ch);
    close(sockfd);
    exit(0);
}
=========================================================

注意上面还有一个将本地字节序转换成网络字节序的操作。这是因为不同的硬件存储数字的方式可能会有差别(有大小端存储的差别),这样以来同一串二进制编码在不同的计算机中可能意味着不同的数据。所以我们在将本地数据发送之前要将其从本地字节序转换成网络字节序,同样的从网络上接受到的数据在使用之前也要将其从网络字节序转换成本地字节序。用来完成这样转换的函数有:

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

在这些函数的命名中可以看出其作用,例如htonl(host to network, long)。在上面的例子中,由inet_addr()返回的地址已经是网络字节序的地址,所以不用转换。而端口号中,只有client端中的端口号需要转换,因为这个端口号会被传输到服务器以供验证,而服务器端的端口号只用于本地验证,不经过互联网传输,所以不需要转换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值