3、【网络编程】Socket编程

一、Socket定义

    Socket:在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯 一标识网络通讯中的一个进程,所以“IP地址+端口号”就称为socket。 在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。 TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。

二、TCP套接字编程模型

1、服务器端流程简:

    (1)创建套接字(socket);

    (2)将套接字绑定到一个本地地址和端口上(bind);

    (3)将套接字设定为监听模式,准备接受客户端请求(listen);

    (4)阻塞等待客户端请求到来。当请求到来后,接受连接请求,返回一个新的对应于此客户端连接的套接字sockClient(accept);

    (5)用返回的套接字sockClient和客户端进行通信(send/recv);

    (6)返回,等待另一个客户端请求(accept);

    (7)关闭套接字(close);

2、客户端流程:

    (1) 创建套接字(socket);

    (2) 向服务器发出连接请求(connect);

    (3) 和服务器进行通信(send/recv);

    (4) 关闭套接字(close);

具体流程如下图所示:

三、Socket基本操作

1、创建套接字,socket函数
    int socket(int domain, int type, int protocol)
    //成功时返回文件句柄,失败时返回-1.
    //domain 套接字中使用的协议族信息
    //type 套接字数据传输类型信息
    //protocol 计算机间通信使用的协议信息

(1)domain所选的协议族

名称 协议族
PF_INET IPV4互联网协议族
PF_INET6 IPV6互联网协议族

(2)type套接字类型

type类型 作用
SOCK_STREAM 面向连接的套接字
SOCK_DGRAM 面向消息的套接字

(3)protacol协议的最终选择

    同一协议中存在多个数据类型传输方式相同的协议,就通过protocol区分最终协议,如果只有一个,默认为0。

2、分配IP和端口,bind函数
    int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen) 
    //成功返回0,失败返回-1 
    //sockfd 套接字文件描述符 
    //myaddr 结构体变量地址值,包括IP地址和端口号 
    //addrlen 结构体变量的长度

    bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

    (1)sockfd:即socket描述字,它是通过socket()函数创建的,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

    (2)addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如IPV4对应的是:

struct sockaddr_in {
    /*address family:AF_INET*/
    sa_family_t sin_family; 
    
    /*port in network byte order */
    in_port_t sin_port; 
    
    /* internet address */
    struct in_addr sin_addr;   
};
/* Internet address. */
struct in_addr {
    /* address in network byte order */
    uint32_t s_addr;     
};

    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 */ 
};

    (3)addrlen:对应的是地址的长度。

    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

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可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求

    connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接

4、accept()函数

     TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

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

     accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

     注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

5、read()/write()函数

    万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

    read()/write()
    recv()/send()
    readv()/writev()
    recvmsg()/sendmsg()
    recvfrom()/sendto()

    开发语言不同可能读写函数也就不同,只要把自己想要发送的消息,以字节流的方式写入Socket或者从Socket读出来即可实现网络的I/O操作。

6、close()函数

    在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    #include <unistd.h>
    int close(int fd);

    close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

    注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

三、Socket编程实例

    咋Linux上实现的一个简单的socket通信实例:
Server端:

#include<stdio.h>
//下面两个头文件是使用socket必须引入的
#include<sys/types.h>
#include<sys/socket.h>

#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
 //启动服务器通信端口
int startup(int _port,const char* _ip)
{
    //socket()函数打开一个网络通信窗口,成功则返回一个文件描述符,应用程序可以向读写文件一样用read/write在网络上转发数据。
    //若调用出错则返回-1
    //socket()函数的三个参数:协议类型, 套接字类型, 协议类型的常量或设置为0
    //AF_INET(IPv4协议)  SOCK_STREAM字节流套接字
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(1);
    }
 
    struct sockaddr_in local;//网络编程中常用的数据结构
    local.sin_family=AF_INET;//IPVC4地址族
    local.sin_port=htons(_port);//将端口地址转换为网络二进制数字
    local.sin_addr.s_addr=inet_addr(_ip);//将网络地址转换为网络二进制数字
 
    socklen_t len=sizeof(local);
   //绑定套接字:成功返回0, 失败返回-1
    //功能:将sock和local绑定在一起,使得sock这个用于网络通讯的问价描述符监听local所描述的地址和端口
   if(bind(sock,(struct sockaddr*)&local,len)<0)
   {
     perror("bind");
     exit(2);
    }
    //listen(int sockfd, int backlog)监听函数,sockfd为要监听的socket套接字,backlog为可以排队的最大连接数。
    //socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
    //监听成功返回0, 失败返回-1
    if(listen(sock,5)<0)
    {
        perror("listen");
        exit(3);
    }
   return sock;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage: [local_ip] [local_port]",argv[0]);
        return 3;
    }
    //启动服务器套接字listen_socket
    int listen_socket=startup(atoi(argv[2]),argv[1]);
 
    struct sockaddr_in remote;
    socklen_t len=sizeof(struct sockaddr_in);
 
    while(1)
    {
    //accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)函数,
    //sockfd为服务器的socket套接字,addr为客户端协议地址,addrlen为协议地址的长度,
    //如果accept成功,则返回一个由内核自动生成的全新套接字,代表与返回客户的TCP连接
        int socket=accept(listen_socket,(struct sockaddr*)&remote,&len);
        if(socket<0)
        {
            perror("accept");
            continue;
        }
    //inet_ntoa:将网络二进制数字转换为网络地址      
    //ntohs:将网络二进制数字转换为端口号
        printf("client,ip:%s,port:%d\n",inet_ntoa(remote.sin_addr)\
               ,ntohs(remote.sin_port));
    
 
        char buf[1024];
        while(1)
        {
        //调用网络I/O进行读写
            ssize_t _s=read(socket,buf,sizeof(buf)-1);
            if(_s>0)
            {
               buf[_s]=0;
               printf("client# %s\n",buf);   
            }
            else
            {
               printf("client is quit!\n");
               break;
            }
            
        }
    //关闭套接字
        close(socket);
    }    
    return 0;
}

Client端:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
 
static void usage(const char* proc)
{
    printf("usage:%s [ip] [port]\n",proc);
}
 
int main(int argc,char* argv[])
{
   if(argc!=3)
    {
        usage(argv[0]);
        return 3;
    }
    
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(1);
    }
 
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    //connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
    //sockfd为要监听的socket套接字,addr参数为服务器的socket地址,addrlen为socket地址的长度。
    //客户端通过调用connect函数来建立与TCP服务器的连接。
    if(connect(sock,(struct sockaddr*)&server,(socklen_t)sizeof(server))<0)
    {
        perror("connect");
        exit(2);
    }
 
    char buf[1024];
 
    while(1)
    {
        printf("send#");
        fflush(stdout);
        ssize_t _s=read(0,buf,sizeof(buf)-1);
        buf[_s-1]=0;
        write(sock,buf,_s);
    }
    
    close(sock);
    return 0;
 
}

猜你喜欢

转载自blog.csdn.net/sinat_33924041/article/details/83861215