SOCKET通信

一、Socket是啥?

二、Socket通信

三、需调用的函数

四、函数在哪?怎么定义?为什么选它?


先解决第一个问题。啥是Socket呢?

       socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

       说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
       注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。

Socket通信

1、网络中进程怎么通信?

 程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如


      UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
      UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.


他们都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。 

其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。


使用TCP/IP协议的应用程序通常采用应用编程接口:

        UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。


 TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。


2、使用socket套接字的套接字描述符

       其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。


      套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。特别是,当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。

      在许多操作系统中,套接字描述符和其他I/O描述符是集成在一起的,所以应用程序可以对文件进行套接字I/O或I/O读/写操作。
      当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。下图显示,操作系统如何把文件描述符实现为一个指针数组,这些指针指向内部数据结构。

   对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。


3、需要使用的基本的SOCKET接口函数

socket()
bind()
connect()
listen()
accept()
send()
recv()

close()

下图为A和B建立连接:


         服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

1. socket()函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain , int type , int protocol);//返回sockfd
首先,domain 需要被设置为 “AF_INET”,就像上面的struct sockaddr_in。然后,type参数告诉内核这个socket 是什么类型,“SOCK_STREAM”或是“SOCK_DGRAM”。最后,只需要把protocol 设置为0 。(AF_前缀代表地址族   PF_前缀代表协议族)
socket()函数只是简单的返回一个你以后可以使用的套接字描述符。如果发生错误,socket()函数返回 –1 。全局变量errno 将被设置为错误代码。

2.bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen)
bind()的系统调用声明如下:
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;
参数说明:
l sockfd 是由socket()函数返回的套接字描述符。
l my_addr 是一个指向struct sockaddr 的指针,包含有关你的地址的信息:名称、端口和IP 地址。
l addrlen 可以设置为sizeof(struct sockaddr)。
当bind()函数调用错误的时候,它也是返回–1 作为错误发生的标志。errn 的值为错误代码。
当你调用bind()的时候,不要把端口数设置的过小!小于1024 的所有端口都是保留下来作为系统使用端口的,没有root 权利无法使用。你可以使用1024 以上的任何端口,一直到65535


对socket进行定位
 
相关函数
socket,accept,connect,listen
表头文件
#include<sys/types.h>
#include<sys/socket.h>
定义函数
int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
函数说明
bind()用来设置给参数sockfd的socket一个名称。此名称由参数my_addr指向一sockaddr结构,对于不同的socket domain定义了一个通用的数据结构
struct sockaddr
{
unsigned short int sa_family;
char sa_data[14];
};
sa_family 为调用socket()时的domain参数,即AF_xxxx值。
sa_data 最多使用14个字符长度。
此sockaddr结构会因使用不同的socket domain而有不同结构定义,例如使用AF_INET domain,其socketaddr结构定义便为
struct socketaddr_in
{
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr
{
uint32_t s_addr;
};
sin_family 即为sa_family
sin_port 为使用的port编号
sin_addr.s_addr 为IP 地址
sin_zero 未使用。
参数
addrlen为sockaddr的结构长度。
返回值
成功则返回0,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数sockfd 非合法socket处理代码。
EACCESS 权限不足
ENOTSOCK 参数sockfd为一文件描述词,非socket。
 
该函数用来指定一个端口号,一个IP地址,两者都指定,或者两者都不指定.可以不使用该函数调用。使用socket()得到套接口后可以直接调用函数conect()或者listen(),这时内核会自动给套接口分配一个地址和端口号(众所周知的端口号),这是常用的方法。只有在进程需要使用特定的网络地址和端口时才会进行绑定,即使用bind()函数。调用bind()的常见错误是EADDRINUSE,即指定的地址正在使用,主要是指定的端口号被使用了,IP地址可以被多个进程使用,但端口在同一时刻只能被一个进程使用。
套接口中port=0表示由内核指定端口号,设定sin_addr为INADDR_ANY(表示任意的意思),就有内核指定端口号。
设置端口为0的语句:
       struct socketaddr_in seeveraddr;
       serveraddr.port = 0;
设置IP的语句:
      serveraddr.sin_addr = htonl(INADDR_ANY);
 
3.  connect() 函数
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
connect()的三个参数意义如下:
l sockfd :套接字文件描述符,由socket()函数返回的。
l serv_addr 是一个存储远程计算机的IP 地址和端口信息的结构。
l addrlen 应该是sizeof(struct sockaddr)。
 
4.  listen()函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen()函数的参数意义如下:
l sockfd 是一个套接字描述符,由socket()系统调用获得。
l backlog 是未经过处理的连接请求队列可以容纳的最大数目。
backlog 具体一些是什么意思呢?每一个连入请求都要进入一个连入请求队列,等待listen 的程序调用accept()(accept()函数下面有介绍)函数来接受这个连接。当系统还没有调用accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是backlog 的数值。你可以将其设成5 到10 之间的数值(推荐)。像上面的所有函数一样, listen()如果返回 –1 ,那么说明在listen()的执行过程中发生了错误。全局变量errno 中存储了错误代码。
 
5.  accept()函数
当调用它的时候, 大致过程是下面这样的
l 有人从很远很远的地方尝试调用connect()来连接你的机器上的某个端口(当然是你已经在listen()的)。
l 他的连接将被listen 加入等待队列等待accept()函数的调用(加入等待队列的最多数目由调用listen()函数的第二个参数backlog 来决定)。
l 你调用accept()函数,告诉他你准备连接。
l accept()函数将回返回一个新的套接字描述符,这个描述符就代表了这个连接!
 
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);
accept()函数的参数意义如下:
l sockfd 是正在listen() 的一个套接字描述符。
addr 一般是一个指向struct sockaddr_in 结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的IP 地址和端口)。
l addrlen 是一个本地的整型数值,在它的地址传给accept() 前它的值应该是sizeof(struct sockaddr_in);accept()不会在addr 中存储多余addrlen bytes 大小的数据。如果
accept()函数在addr 中存储的数据量不足addrlen,则accept()函数会改变addrlen 的值来反应这个情况。
 
6.  send()、recv()函数
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
send 的参数含义如下:
l sockfd 是代表你与远程程序连接的套接字描述符。
l msg 是一个指针,指向你想发送的信息的地址。
l len 是你想发送信息的长度。
l flags 发送标记。一般都设为0(你可以查看send 的man pages 来获得其他的参数
值并且明白各个参数所代表的含义)。
 
send()函数在调用后会返回它真正发送数据的长度
注意:send() 所发送的数据可能少于你给它的参数所指定的长度!因为如果你给send()的参数中包含的数据的长度远远大于send()所能一次发送的数据,则send()函数
只发送它所能发送的最大数据长度,然后它相信你会把剩下的数据再次调用它来进行第二次发送。所以,记住如果send()函数的返回值小于len 的话,则你需要再次发送剩下的数据。幸运的是,如果包足够小(小于1K),那么send()一般都会一次发送光的。像上面的函数一样,send()函数如果发生错误,则返回 –1 ,错误代码存储在全局变量errno 中。
 
函数recv()调用在许多方面都和send()很相似,下面是recv()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, int len, unsigned int flags);
recv()的参数含义如下:
l sockfd 是你要读取数据的套接字描述符。
l buf 是一个指针,指向你能存储数据的内存缓存区域。
l len 是缓存区的最大尺寸。
l flags 是recv() 函数的一个标志,一般都为0 (具体的其他数值和含义请参考recv()
的man pages)。
recv() 返回它所真正收到的数据的长度。(也就是存到buf 中数据的长度)。如果返回–1 则代表发生了错误(比如网络以外中断、对方关闭了套接字连接等),全局变量errno 里面存储了错误代码。
7.  close()

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

执行close()之后,套接字将不会在允许进行读操作和写操作。任何有关对套接字描述符进行读和写的操作都会接收到一个错误。

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

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

8.htonl()

htons( u_short hostshort);
hostshort:主机字节顺序表达的16位数。
注释:
本函数将一个16位数从主机字节顺序转换成网络字节顺序。
返回值:
htons()返回一个网络字节顺序的值。

9.inet_pton()

#include<arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:  

inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。

第一个参数af是地址族,转换后存在dst中
    af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
  af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。


代码:

/*********************************************************************************
 *      Copyright:  (C) 2016 xiaokai<942286384@qq.com>
 *                  All rights reserved.
 *
 *       Filename:  sever.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(01/16/2016)
 *         Author:  xiaokai <942286384@qq.com>
 *      ChangeLog:  1, Release initial version on "01/16/2016 05:34:45 PM"
 *                 
 ********************************************************************************/
#include<stdio.h> 
#include<stdlib.h>  
#include<string.h>
#include<errno.h>  
#include<unistd.h>
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>
//服务器要监听的本地端口
#define DEFAULT_PORT 8000  
//accept()能够监听端口数
#define BACKLOG 10
#define MAXLINE 4096 

int main(int argc, char** argv)  
{  
    int    socket_fd;//在socket_fd上进行监听
    int    connect_fd;  //接受新的连接
    struct sockaddr_in     servaddr;  //服务器的地址信息
    char    buff[4096];  
    int     n; //返回存到buf中数据的长度
//初始化Socket ,强调首先进行错误检查,若调用socket()出错,则返回
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){  
        //输出错误信息并退出
         printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
         exit(0);  
     }
    //初始化,给服务器分配地址空间
    memset(&servaddr, 0, sizeof(servaddr));  
    //主机字节
    servaddr.sin_family = AF_INET;  
    //得到网络地址
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT 
    //将本地地址绑定到所创建的套接字上
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){  
        //首先进行错误检测
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
        exit(0);  
    }
    //开始监听是否有客户端连接  
    if( listen(socket_fd, BACKLOG) == -1){  
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
        exit(0);  
    }  
    printf("======waiting for client's request======\n");  
    while(1){ 
        //阻塞直到有客户端连接,不然多浪费CPU资源。
        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);  
        //向客户端发送回应数据
        //这里将建立一个子进程和刚刚建立的套接字进行通讯
        if(!fork()){ 
            //这里是子进程,进行错误检测
           if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)  
               //若出错,给出提示,然后关闭这个刚刚建立的新连接并退出!
                                perror("send error");  
           close(connect_fd);  
           exit(0);  
        }  
        buff[n] = '\0';  
        //输出客户端的信息,并关闭connect_fd为代表的所有连接套接字的连接
        printf("recv msg from client: %s\n", buff);  
        close(connect_fd);  
    }
    //关闭监听套接字
    close(socket_fd);  
}  

/*********************************************************************************
 *      Copyright:  (C) 2016 Xiao Kai
 *                  All rights reserved.
 *
 *       Filename:  client.c
 *    Description:  This file is a client.
 *                 
 *        Version:  1.0.0(01/16/2016)
 *         Author:  Xiao Kai <942286384@qq.com>
 *      ChangeLog:  1, Release initial version on "01/16/2016 04:09:51 PM"
 *                 
 ********************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<arpa/inet.h>

#define MAXLINE 4096


int main(int argc, char** argv)
{
    int    sockfd, n ,rec_len;
    char    recvline[MAXLINE], sendline[MAXLINE];
    struct sockaddr_in    servaddr;
    //若输入指令少于2个,给出提示!
    if( argc != 2){
        printf("usage: ./client <ipaddress>\n");
        exit(0);
       }
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
        exit(0);
       }
    //给服务器地址分配内存
        memset(&servaddr, 0, sizeof(servaddr));
    //给出主机字节
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(8000);
        //ip地址转化函数,可以将ip地址在点分十进制和整数之间进行转换,argv[1]为指向字符串的地址,即ASCII地址的首地址
        //(xxx.xxx.xxx格式),若出错,则返回负值并打印ip地址
        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);
          //发送后检测发送的长度,若小于o则出错
          if( send(sockfd, sendline, strlen(sendline), 0) < 0)
          {
              printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
              exit(0);
          }
          //关闭所有监听套接字
          close(sockfd);
          exit(0);
 }

测试:

编译sever.c


等待客户端响应,先编译client.c


此时服务器上接收到:


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值