一个简单的socket服务器实现

萝らか妹 提交于 2020-03-05 07:36:18

目录


  • 什么是socket
  • socket基本操作API函数
  • socket服务器的实现
  • 附录:网络字节序与主机字节序

什么是socket


socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现,socket是操作系统抽象出一个概念,连接传输层与应用层的上层应用在这里插入图片描述如图中所示套接字(socket)是一个抽象层,继承了Linux下“万物皆文件”的思想,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合


socket基本操作API函数


1. socket()函数
函数原型及所需的头文件
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
(1)作用:socket()函数用来创建一个socket描述符,socket描述符类似于普通的文件描述符,可以进行读写操作,一个socket描述符唯一指定一个socket
(2)三个参数:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)

protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议

(3)返回值:socket()调用成功时,返回socket描述符;调用失败时返回一个负值,可调用strerror(errno)函数查看错误原因

2. setsockopt()
作用:在网络socket服务器编程时避免端口占用,并发服务器中要重复使用端口,通过如下语句设置端口重用(单线程服务器中一般不考虑端口重用所以不需要使用此语句)

int on=1;
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

3. bind()函数
所需的头文件与
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)功能:将socket与IP和端口绑定
(2)参数:第一个参数即为socket()返回的socket描述符;第二个参数为const struct sockaddr*类型的指针,sockaddr为通用套接字,其定义为

struct sockaddr {    
    sa_family_t sa_family;          // 地址族    
    char        sa_data[14];        // 存放socket地址值
 };`

因使用的地址协议族不同,实际使用的套接字多种多样,如sockaddr_in,sockaddr_in6,sockeaddr_un(分别对应IPv4,IPv6以及Unix域通信协议),赋值时都须通过强制类型转换转换为sockaddr类型传参;第三个参数为对应地址的长度,需要注意的是,1024以下的端口是系统占用的端口,调用时需要root或sudo权限,一般避免使用1024以下的端口
(3)返回值:如无错误发生,则bind()返回0。否则的话,将返回-1

4. listen()函数
int listen(int sockfd, int backlog);
开始监听套接字
sockfd是socket描述符,backlog为相应socket可以在内核里排队的大连接个数

5. accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
(1)作用:用来等待客户端的连接,默认为阻塞函数,只有客户端调用connect()函数时才会触发accept()函数
(2)参数:sockfd:由socket创建的socket描述符
addr:用于返回客户端的协议地址,这个地址里包含客户端的IP和端口等信息
addrlen:返回的客户端协议地址的长度
(3)返回值:由内核生成的一个全新的描述字(fd),代表与返回客户端的TCP相连,可以通过对其进行读写操作向客户端读写

6. connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)作用:客户端向服务器端进行连接
(2)参数:sockfd:客户端的socket描述符
addr:要连接的服务器端的地址信息,这个地址里包含客户端的IP和端口等信息
addrlen:服务器协议地址的长度
(3)返回值:成功返回0,失败返回-1


socket服务器的实现


客户端与服务器通信的示意图如下
在这里插入图片描述
由此可见在单线程服务器中API函数的执行流程是:socket()->bind()->listen()->accept()->close()
下面给出一个简单的socket服务器的实现代码

int main()
{
  int sockfd         =-1;
  int client_fd      =-1;
  struct sockaddr_in serv_addr;
  struct sockaddr_in cli_addr;
//  int serv_port      =6666;   
//  char* serv_ip      ="127.0.0.1";    
  socklen_t          cliaddr_len; 
  int rv             =-1;
  char               buf[1024];
  
  sockfd=socket(AF_INET,SOCK_STREAM,0);
  if(sockfd<0)
  {
      printf("create sockfd failure: %s\n",strerror(errno));
      return -1;
  }
  printf("create sockfd successfully! \n");
 
  memset(&serv_addr,0,sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(LISTEN_PORT);
  serv_addr.sin_addr.s_addr = INADDR_ANY;

  rv=bind(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
  if(rv<0)
  {
      printf("bind failure: %s\n",strerror(errno));
      return -2;
  }
  printf("bind successfully \n");

  listen(sockfd,BACKLOG);

  while(1)
  {
      printf("\n%d start waiting and accepting new client connect... \n",sockfd);
      client_fd=accept(sockfd,(struct sockaddr *)&cli_addr,&cliaddr_len);
      if(client_fd<0)
      {
          printf("connect failure: %s\n",strerror(errno));
          return -3;  
      }
      printf("accept new client with fd \n");

      memset(buf,0,sizeof(buf)); 
      rv=read(client_fd,buf,sizeof(buf));
      if(rv<0)
      {
          printf("connect to client error: %s\n",strerror(errno));
          close(client_fd);
          continue;
      }
      else if(rv==0)
      {
          printf("client connect to server get disconnected \n");
          close(client_fd);
          continue;
      }
      printf("get %d Bytes from client \n",rv);
      
      rv=write(client_fd,MSG_STR,strlen(MSG_STR));
      if(rv<0)
      {
          printf("write %d Bytes to client error: %s\n",rv,strerror(errno));
          close(client_fd);
      }
      printf("write %d Bytes to client successfully! \n",rv);
      sleep(1);
      close(client_fd);
  }
  close(sockfd);
  return 0;
}

记录一下程序编写时遇到的错误:
1.编译器提示此条目为库函数头文件未正确包含或库函数名称拼错
编译器提示此条目为库函数头文件未正确包含或库函数名称拼错
2.字符串求长度用strlen函数不能用sizeof函数

需要注意的地方
1.127.0.0.1为本机IP;INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思
2.accept函数最后一个参数应先在主函数中定义socklen_t类型变量cliaddr_len接收connect函数的传参,再传地址&cliaddr_len传参给accept函数
3.定义的结构体和缓冲区要先调用memset清空,避免出现随机值
4.
如下面的代码中所示,对IPv4协议套接字结构体进行初始化时要注意其中的成员sin_addr为内嵌结构体,要访问其内部成员s_addr对其赋值

memset(&serv_addr,0,sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(LISTEN_PORT);
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

同时赋值时要注意调用htons()和htonl()进行网络字节序与主机字节序的转换,那么什么是网络字节序与主机字节序,将在下面介绍

附录:网络字节序与主机字节序

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序, 一个字节的数据没有顺序的问题了

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端 b)
Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端

网络字节序: 4个字节的32 bit值以下面的次序传输:首先是0 ~ 7bit,其次8~ 15bit,然后16~
23bit,最后是24~31bit。
这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序

所以,在将一 个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian

可以通过调用两个函数htons()和htolnl()分别用来将端口和IP地址转换成网络字节序,这两个函数名中的h表示host,
n表示network, s表示short(2字节/1
6位),表示long(4字节/32位)。因为端口号是16位的,所以我们用htons(把端口号从主机字节序转换成网络字节序,而IP地址是32位的,
所以我们用htonl()函数把IP地址从主机字节序转换成网络字节序

 serv_addr.sin_port = htons(LISTEN_PORT);
 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

上述两个函数只适用ipv4,下面两个函数兼容ipv4和ipv6 inet_pton()和inet_ntop(),因IPv4地址逐渐枯竭,推荐使用这两个函数

inet_pton函数所需头文件:
#include <sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
函数原型:

int inet_pton(int af, const char *src, void *dst);

(1)参数:第一个参数af是地址簇;src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
(2)返回值:如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0
(3)调用伪代码:

char IPdotdec[20]; //存放点分十进制IP地址
struct in_addr s; // IPv4地址结构体
inet_pton(AF_INET, IPdotdec, (void *)&s);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!