Linux Socket编程入门——浅显易懂

1. 概述

  网络通信,首先那些七层模型等概念,小的不才,之前有写过几篇关于网络的文章,如果有时间,可以去看看,浅显易懂。可能写的不好,但这里只是个人的一些见解吧。


2. Socket

  Socket本身有“插座”的意思,在Unix/Linux环境下,用于表示进程间网络通信的特殊文件类型(Linux下一切皆文件)。本质为内核借助缓冲区形成的伪文件。那么就可以使用文件描述符引用套接字, 与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

  在TCP/IP协议中,"IP地址+TCP或UDP端口号"唯一标识网络通讯中的一个进程。"IP地址+端口号"就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。(左边是server端的套接字,右边是client端的套接字)

在这里插入图片描述
  我觉得对于Socket通信,网上的很多文章没有讲到一个很重要的点。对于一个Server和一个Client端单线通信,Server端会有两个套接字,而Client端只有一个。为什么Server端会有两个呢?Server端其中一个就是用来和Client端通信的。另一个是用来监听的,以防会有其他Client端想与Server端创建连接。

简单的来说
  在Server端,socket()返回的套接字(lfd)用于监听(listen)和接受(accept)Client端的连接请求。这个套接字不能用于与Client端之间发送和接收数据。当连接成功后,会返回一个新的套接字(cfd),Server端是用这个新的套接字与Client端通信。而之前的那个套接字(lfd),会接着去与其他Client端进行连接,连接后再返回一个新的套接字…


3. 网络字节序

  不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。TCP/IP协议规定,网络字节序统一为大端序
  主机A先把数据转换成大端序再进行网络传输,主机B收到数据后先转换为自己的格式再解析。
  为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>
/*主机字节顺序 --> 网络字节顺序*/
uint32_t htonl(uint32_t hostlong); /*转32位,IP是32位,所以是针对的是IP*/     
uint16_t htons(uint16_t hostshort);/*转16位,针对的是端口*/  

/*网络字节顺序 --> 主机字节顺序*/
uint32_t ntohl(uint32_t netlong);  /*IP*/
uint16_t ntohs(uint16_t netshort); /*端口*/

  对于Server端,可以使用INADDR_ANY宏,代表从本地取一个有效的无符号整数的IP地址,一步到位。
  而对于客户端,绑定Server端的IP,要从String->int->htonl 转换,很麻烦,所以,就有了下面两个网络地址转换函数

#include <arpa/inet.h>

/*点分十进制的IP转换为网络字节序*/
int inet_pton(int af, const char *src, void *dst);
/**   
   *  af   - 地址族协议对应的有AF_INET, AF_INET6等
   *  src  - 传入参数,点分十进制的IP地址
   *  dst  - 传出参数,转换后的网络字节序的 IP地址
   * 返回值: 成功返回1
   *         异常返回0,说明src指向的不是一个有效的IP   
   *    	 失败返回-1	  
  */
  
/*网络字节序转换为点分十进制的IP*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/**   
   *  af   - 地址族协议对应的有AF_INET, AF_INET6等
   *  src  - 传入参数,网络字节序的IP地址
   *  dst  - 传出参数,转换后的本地节序的 (string IP)
   *  size - dst缓冲区的大小
   * 返回值: 成功返回dst
   *    	 失败返回NULL  
  */

4. sockaddr 数据结构

可以使用 [ man 7 ip ]命令查看

  还要了解sockaddr数据结构, const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同使用不同结构体。原来的结构体即左边第一个, 这种使用不方便,所以出现针对不同类型协议的结构体,可以直接访问其对应的值,主要是IP和port(端口)。

在这里插入图片描述
常用的是ipv4

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

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

#define UNIX_PATH_MAX    108
struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

5. 网络套接字API函数

 5.1 socket()

查看帮助:man socket
原型:int socket(int domain, int type, int protocol)
函数作用:为通讯创建一个套接字,返回该套接字的文件描述符
函数返回值:成功,返回新套接字所对应的文件描述符 。失败返回-1
参数:

  • domain:为创建的套接字指定协议集
    AF_INET 表示IPv4网络协议
    AF_INET6 表示IPv6
    AF_UNIX表示本地套接字
  • type:套接字类型
    SOCK_STREAM:流式套接字,提供一个面向连接、可靠的数据传输服务(TCP)
    SOCK_DGRAM:数据报式套接字,提供无连接服务(UDP)
    SOCK_RAW:原始套接字
  • protocol:通常赋值 “0”,根据前面指定的套接字类型,来选择代表传输协议。

 5.2 bind()

查看帮助:man 2 bind
原型: int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数作用:给Socket绑定一个地址结构(IP+端口号),地址结构在sockaddr_in这个结构体中。
函数返回值:成功返回0,失败返回-1
参数:

  • sockfd:调用socket函数返回的socket文件描述符
  • addr:一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针
  • addrlen:sockaddr结构的长度,sizeof(addr)

 5.3 listen()

查看帮助:man listen
原型:int listen(int sockfd, int backlog);
函数作用:设置同时与服务器建立连接的上限数
函数返回值:函数成功返回0,失败返回-1
参数:

  • sockfd:调用socket函数返回的socket文件描述符
  • backlog:backlog对队列中等待服务的请求的数目进行了限制,最大值是128

 5.4 accept()

查看帮助:man 2 accept
原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数作用:阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接Socket文件描述符
函数返回值:成功,返回能与服务器进行数据通信的Socket对应的文件描述符;
      出错返回-1

  • sockfd:调用socket函数返回的socket文件描述符
  • addr:传出参数,成功与服务器建立连接的那个客户端的地址结构
  • addrlen:传入、传出参数。传入addr的大小,传出客户端addr实际大小。如果我们对客户端的协议地址不感兴趣,可以把arrd和addrlen均置为空指针。

 5.5 connect()

查看帮助:man 2 accept
原型:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数作用:使用现有的Socket与服务器建立连接
函数返回值:

  • sockfd:调用socket函数返回的socket描述符
  • addr:传入参数,服务器的地址结构
  • addrlen:服务器的地址结构的大小

C/S的一个基本模型
在这里插入图片描述

6. 一个简单的C/S模型代码实例

  代码作用:一个客户端与一个服务端建立连接,客户端输入小写字母,传输到服务端。服务端打印客户端的IP和端口号,再把收到的小写字母转换为大写字母,再传给客户端。

/***
Server端

Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/ 

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/wait.h>
#define SERV_PORT 10005
#define BUFIZE 4096

  void sys_err(const char *str){
	perror(str);
	exit(-1);
   } 

 int main(int argc,char *argv[] ){

        int lfd=0,cfd=0;
        int ret,i;
        char buf[BUFIZE];
	    char client_IP[BUFIZE];

        struct sockaddr_in serv_addr,clt_addr;
        socklen_t clt_addr_len;   //客户端地址结构长度 
        
        //服务端建立一个用来监听的Socket套接字lfd
	    lfd = socket(AF_INET,SOCK_STREAM,0);
        if(lfd<0)
          sys_err("socket error");
            
        bzero(&serv_addr,sizeof(serv_addr));   //清空服务端的地址结构 
        
        //设置服务端的地址结构
        serv_addr.sin_family=AF_INET;   //绑定的协议
        serv_addr.sin_port=htons(SERV_PORT);  //绑定的端口号
        serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); //绑定的IP
        //INADDR_ANY宏,代表从本地取一个有效的IP地址

		//绑定地址结构
      if(bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr))==-1)   
        sys_err("Bind error.\n");
       
       //设置监听上限
      if(listen(lfd,10)==-1)
        sys_err("Listen error");
      else
   	    printf("Start to listen!\n");
        
       clt_addr_len=sizeof(clt_addr);
       
       //阻塞等待客户端建立连接,建立连接成功,返回一个专用套接字cfd,用来与client端通信
       cfd=accept(lfd,(struct sockaddr *)&clt_addr,&clt_addr_len);
 	   if(cfd==-1)
 		 sys_err("accept error");
 		 
       //打印客户端的IP、端口号
	   printf("client ip is:%s   client port:%d\n",inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),ntohs(clt_addr.sin_port));
 		 
     //循环的读缓冲区内的内容
    while(1){
	   ret = read(cfd,buf,sizeof(buf));
	    //检测到客户端关闭 
       if (ret==0){  
         printf("client has been drop out.\n");
	     close(cfd);
         exit(-1);
        }
        
        //小写转大写
        for(i=0;i<ret;i++)
           buf[i]=toupper(buf[i]);  
           
	    printf("What to send to the client:\n");
        //写回客户端   
        write(cfd,buf,ret);  
        
        //为了验证转换是否正确,将转换后的结果输出在server端的屏幕上,这步可以不需要~~
	   write(STDOUT_FILENO,buf,ret); 
	    printf("\n");
 		}

        close(lfd); //关闭套接字lfd
        close(cfd); //关闭套接字cfd
 return 0;
}

/***
client端

Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/ 
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include<arpa/inet.h>
#define SERV_PORT 10005
#define CONNECT_NUM 5
#define BUFIZE 4096
#define MAX_NUM 80

void sys_err(const char *str){
	perror(str);
	exit(-1);
} 

int main(void){

		int cfd;
		struct sockaddr_in serv_addr;  //服务器地址结构 
		int ret;
		serv_addr.sin_family=AF_INET;
		serv_addr.sin_port=htons((u_short) SERV_PORT);
	  
       //网络字节序转本地字节序 
	   inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr.s_addr);   //绑定的IP应该是服务器的IP 

	   cfd = socket(AF_INET,SOCK_STREAM,0);
	     if(cfd<0)
          sys_err("socket error");

	   if(connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr))<0)
    	 sys_err("connect errpr"); 
    	
       printf("Connect successful.\n");


       char sedBuf[MAX_NUM]={0};  //发送缓冲区
       char revBuf[MAX_NUM]={0};  //接收缓冲区

      //只要客户端不关闭,就一直循环
	   while(gets(sedBuf)!=-1){
	   
	  //阻塞写,将客户端写的内容cpoy到发送缓冲区中,如果不写,就一直阻塞在这
	  if(write(cfd,sedBuf,strlen(sedBuf))<0)  
	    sys_err("write error");
	 
       bzero(sedBuf,sizeof(sedBuf));   //清空缓冲区
        
        //从套接字cfd中的接收缓冲区中,读服务端发来的内容
	    if(read(cfd,revBuf,sizeof(revBuf))<0)
	      sys_err("read error");
	    else      
          printf("Sever:%s\n",revBuf);  //打印
        
        bzero(revBuf,sizeof(revBuf));   //清空缓冲区
	}
	close(cfd);
	return 0;
}

在这里插入图片描述
用XShell,来连接虚拟机,建立一个虚拟Client
在这里插入图片描述

7.总结

  文章很浅显的讲解了Linux下的Socket编程,例子很简单,只是为了能够更好的了解Socket编程。下面几篇文章,是关于多线程、多进程,阻塞、非阻塞的Socket编程,包括聊天室等。敬请关注!!

    在这里插入图片描述

发布了36 篇原创文章 · 获赞 65 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43275558/article/details/105017438