Mki's Blog

网络:Socket套接字编

写在开头

  关于网络的模型,有OSI七层模型,或者TCP/IP模型。一般来说,OSI参考模型其实只是一种理想化的模型,现实情况下,真正的网络构架其实不是这样的。更契合的是TCP/IP模型。而在这个模型中,应用层和运输层的信息交互是很重要的一环。先不管复杂的其他细节是什么样的,今天先讲一讲关于应用层和传输层之间的套接字以及socket编程(此处先用C写了,之前写过一版Python的),后面会整理下最近学的网络。

套接字

定义

  套接字(Socket)的定义,是说主机的IP加上端口号来表示TCP连接的端点。

  不难想象,在互联网上,你假如要向某个地方发送一段信息,光是凭一个IP地址,最多也就是找到相对应的主机,可是主机上这么多的进程,这信息难道给所有的进程都来一条?显然这是不可能的。

  为此,我们需要一个"IP+端口号"的组合,这样子就可以顺利的把信息交给正确的处理人。 socket2.png

  我们所说的套接字其实我以前一直以为Socket是袜子的意思,它位于应用层和传输层之间,事实上,它也是一种应用编程接口:意思就是说,你不用管我包装的东西是怎么运作的,实现细节不需要你去操心,你要关注如何正确使用我。

  而在Windows下,使用的套接字叫WinSock,在Unix下直接叫Socket,这里因为现在服务器用的大多是linux,所以我就用Socket实现了。

套接字通信的过程

原理上的流程是这样的 socket文字流程.jpg

  涉及到相关的函数表述即为 socket3.jpg

  在上面两个图中,只有一次信息的发送和读取,事实上这里可以循环发送信息和接受信息的过程,多次读取接收。

下面讲一下这里涉及到的几个函数

函数解析

1.Socket()函数

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

函数作用: 创建一个socket, 并返回一个整型地址指针。

protofamily:协议族,用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等,用来决定地址的类型。

type:socket类型,有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。

protocol: 与type对应的协议,有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。(假如这里写0那就是选默认对应类型)。

返回值: 整型socket描述符,是一个指向内部数据结构的指针。

socket_fd = socket(AF_INET, SOCK_STREAM, 0)

AF_INET表示这是IPV4的协议族,用了SOCK_STREAM类型,匹配与其对应的协议。

socket_fd是一个socket描述符,而它指向的socket结构体是这样的(这不重要,了解一下)

struct socket
{
     socket_state  state;  // socket state

     short   type ;        // socket type

     unsigned long  flags; // socket flags

     struct fasync_struct  *fasync_list;

     wait_queue_head_t wait;

     struct file *file;

     struct sock *sock;    // socket在网络层的表示;

     const struct proto_ops *ops;

}

2.bind()函数

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

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该函数的作用是把地址族里面的一个地址绑定给socket。

sockfd:socket描述符。 addr:addr是一个指针,指向sockfd要绑定的地址。 addrlen:对应地址长度。 返回值: 绑定失败返回-1,成功返回0。

例如:

bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr);

在这里的socket_fd就是前面产生的描述符地址,(struct sockaddr*)表示一个结构体指针,&servaddr取了servaddr的地址,sizeof(servaddr)就是该地址的长度。

而servaddr这个结构体长这样

struct sockaddr {  
        unsigned short sa_family;//通信类型
        char sa_data[14];        //包含目标套接字的IP和端口信息。         
}; 

但是一般来说我们不用servaddr,这个结构体有缺陷,因为它把地址和端口混合在一起存放,会造成麻烦。

struct sockaddr_in{  
        short sin_family;       //sin_family指代协议族,在socket编程中只能是AF_INET
        unsigned short sin_port;//存储端口号(使用网络字节顺序)
        struct in_addr sin_addr;//存储IP地址,使用in_addr这个数据结构
        char sin_zero[8];       //是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
}; 

而sockaddr_in这个结构体把端口和IP地址分开储存,解决了这个问题。

这里有个网络字节序和主机字节序的区别。

  字节序的意思是证书在内存上的储存顺序。在网络上(例如TCP传输)用的就是网络字节序(NBO),又叫大端模式(Big endian),而在自己的电脑上数据的储存一般是主机字节序(HBO),也叫小端模式(Little endian)。 不同机器的HBO不相同,跟cpu的设计有关,而NBO是相同的。因此在传输的时候我们需要把信息由HBO转换成NBO。

3.listen()函数

函数原型

#include<sys/socket.h>

int listen(int sockfd, int backlog)

sockfd是描述字。 后者backlog是同时能接收的最大连接数。 返回值-1表示失败,0表示成功。 例如:

listen(socket_fd, 10);

  listen()函数把主动型的socket变成了被动性的socket。刚开始的时候,你创建了套接字,其实主机也傻乎乎不知道你是要给人家发消息(使用connect()),还是要接受消息(使用recv()),然鹅,当你使用了listen(), 主机就明白了,你是要接收,于是此时的socket套接字就变成了被动型,也就是说使得原本的进程可以接受其他进程的请求。

  这里的backlog也要注意,一般来说,在网络中很容易出现多个客户端向主机发送请求的状况,而TCP连接的建立也是一个过程,可能一时连接很多,而先前刚连接好的服务器进程也还没来得及去做出反应,这时内核就会搞一个类似队列的东西,来维护和跟踪这些连接好但是没来得及反应的进程(以便后面给他们作出回应),显然,这个队列的数量不能太大,所以一般来说我们会把这个值设定在30以内。

事实上这个进程此时就已经被阻塞了,等待一个请求的到来

4.accept()函数

函数原型

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

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

  这个函数的参数和前面的bind()有点像,addr是指向一个套接字结构体的指针,这个指针指向我们接收到的信息的来源主机上的socket套接字结构体,假如没有的话就设为NULL。 addrlen是局部整型变量,是结构体的大小。假如addr为NULL的话它也为NULL。

例如:

connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL);

  这里的accept函数,从之前的队列(backlog那里)取出最上面的一个,新建了一个socket,向取出的请求发送接受信息,然后把新的socket给了connect_fd,而原来的socket还在listen的状态,等待一个连接请求。

  这里要注意,先是服务器调用listen(),进入阻塞状态;然后客户端调用connect()函数(发送一个握手信息),也进入阻塞状态;服务器接收到消息,accept()发送信息(握手信息),客户端的connect()函数接收到,成功建立连接。也就是说,在这三次握手里面,第二次握手后connect返回值,第三次握手后accept()返回值。

5.connect()函数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

熟悉的参数。。不解释了。

例子:

connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

这是客户端程序调用connect()函数来向服务器建立连接。

6.I/O操作函数

  归根到底,套接字其实遵从了Unix/Linux “一切皆文件”的原则。socket其实也是一种文件,自然,我们对文件可以进行读写操作。而可供我们选择的I/O函数就有很多对, 例如: 1. read()/write() 2. recv()/send() 3. readv()/writev() 4. recvmsg()/sendmsg() 5. recvfrom()/sendto()

这里不全都讲了。讲一下常用的

#include <sys/socket.h>

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

这是最基本的两个I/O函数。 read()函数负责从fd中读取数据,返回值是读取到的字符数目,假如为0的话表示读到了末尾,负数表示读取错误。 fd是描述符,buf指向一个用来储存字符的数组,nbyte是读取的字符数目。

write()函数负责向buf中写入nbytes字节的数据,返回成功写入的字节数,-1表示写入错误,并设置errno变量。

#include <sys/socket.h>

Int recv(int fd,void *buf,int len,int flags)
Int send(int fd,void *buf,int len,int flags)

这里的flag一般设置为0,和前面那一组差不多。 其他几组函数和read() write()本质上是差不多的,就是多了几个参数。

套接字代码实现

/*file_name:server.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 4399
#define MAXLINE 4096

int main(int argc, char** argv){
        int socket_fd, connect_fd;
        struct sockaddr_in servaddr; //sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义
        char buff[4096];
        int n;

        if(( socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ 
        // socket_fd是socket描述字(这是一个结构体
        // 用了AF_INET协议(IPV_4):协议族,第二个参数是socket类型,第三个是协议,0表示类型默认配对协议
                printf("create cocket error: %s(error: %d)\n", strerror(errno),errno);
                exit(0);
        }

        memset(&servaddr, 0, sizeof(servaddr));
        // void *memset(void *s,int c,size_t n)   把&servaddr开始的sizeof(servaddr)大小的内存全部设置为0;
        // memset()的深刻内涵:用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为‘ ’或‘/0’;例:char a[100];memset(a, '/0', sizeof(a));

        //设置一波结构体
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(DEFAULT_PORT);

        //假如绑定失败         //需要一个指针  //这是取地址     //对应地址长度
        if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        // bind()函数就是把一个特定的地址和socker_fd绑定
                printf("bind socket error: %s(error: %d)\n",strerror(errno),errno);
                exit(0);
        }

        //将主动型的socket变成被动型的   第一个参数是描述字 第二个是最大连接个数
        if( listen(socket_fd, 10) == -1){
                printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
                exit(0);
        }

        printf("=====waiting=====\n");


        while(1){
                if((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
                        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
                        continue;
                }


                n = recv(connect_fd, buff, MAXLINE, 0);
                // 所以fork() 实际上有返回值, 而且在两条进程中的返回值是不同的, 在主进程里 fork()函数会返回主进程的pid,   而在子进程里会返回0!  
                if(!fork()){
                        if(send(connect_fd, "hello, you are connected!\n", 26, 0) == -1)
                                perror("send error");
                        close(connect_fd);
                        exit(0);
                }

                buff[n] = '\n';
                printf("recv msg from client: %s\n", buff);
                close(connect_fd);
        }
        close(socket_fd);
}
/*file_name:client.c*/
#include<arpa/inet.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv){
        int sockfd, n, rec_len;
        char recvline[4096], sendline[4096];
        char buf[MAXLINE];
        struct sockaddr_in servaddr;

        if( argc != 2){
                printf("usage: ./client <ipaddress>\n");
                exit(0);
        }

        if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
                printf("create socket error: %s(error: %d)\n", strerror(errno), errno);
                exit(0);
        }

        memset(&servaddr, 0, sizeof(servaddr));/。初始化内存,全部变成0
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(4399);
        // int inet_pton(int af, const char *src, void *dst);
        //转换字符串到网络地址:
        if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
                printf("inet_pton error for %s\n", argv[1]);
                exit(0);
        }

        if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
                printf("connect error: %s(errno: %d)\n", strerror(errno),errno);
                exit(0);
        }

        printf("send msg to server: \n");
        fgets(sendline, 4096, stdin);


        if( send(sockfd, sendline, strlen(sendline), 0) < 0){
                printf("send msg error: %s(errno: %d)\n", strerror(errno),errno);
                exit(0);
        }

        if((rec_len = recv(sockfd, buf, MAXLINE, 0)) == -1){
                perror("recv error");
                exit(0);
        }


        buf[rec_len] = '\0';
        printf("Received: %s ",buf);
        close(sockfd);
        exit(0);
}

运行

本地编译两个.c文件后

./server
./client 127.0.0.1

就可以交互了。

写在最后

  这篇文章写了差不多两天....还只是本地交互,下次出个非本地的(话说之前写的Python那版就是,然鹅我找不到源码了,好多东西都忘了....

  初探网络,本来还想写点前面的基础知识,不过这篇太长了,还是放到下一篇去吧;and最近天天熬夜看超炮,只是突然倍感无趣。

  杭州最近降温了,很快就能穿得厚厚的然后喝热乎乎的牛奶了。

  寒冷的天气赖床也太舒服了吧。

  要是还能吃好吃的并且清空购物车就好了。

  但是生病就不好了,要多喝热水。