【网络编程入门】使用socket在Linux下实现即时通信软件

使用socket在Linux下实现即时通信软件

在前一篇文章中讲到了如何使用winsock:【网络编程入门】在C++中使用Windows TCP Sockets,也算是勉强入门了吧,接下来自己写一下在Linux下的网络编程,也算是把知识理一遍。代码架构参考了实验楼的C++ 实现即时通信软件

  • 使用socket在Linux下实现即时通信软件
    • 介绍
    • 进程、端口、Socket、IP地址
    • 字节顺序
    • 开启Socket
      • socket()
      • bind()
      • listen()
    • 连接服务器端
      • connect()

介绍

进程、端口、Socket、IP地址

首先我们知道两个进程要通信,必须要求进程有唯一标识,本地进程的通信使用PID作为标识,在Linux下使用ps -A即可查看所有进程的PID,但是在网络通信中PID无法保证唯一,所以前人采用了 IP地址+协议+端口号 来标识网络中的一个进程,那么socket就是用于连接这一标识的具体对象。详见简单理解Socket和维基百科。

当然,一台计算机可以建立多个连接,所以有了端口(port),可以使用netstat -pan | grep 端口号命令查看占用某端口的进程

一些常见服务使用的端口如下:

端口 服务
7 Ping
13 Time
15 Netstat
22 SSH
23 Telnet
25 SMTP(发邮件)
80 HTTP(网页)
110 POP(收邮件)

IP地址是分配给网络中每台计算机的身份标识,在Linux下可以使用ifconfig来查看。
因为网站使用数字标识不利于人的记忆,所以大佬们提出了“域名”这一解决方案,使用www.baidu.com这样的简易的名称代替IP地址。当我们在浏览器输入这些域名时,将通过路由器查找该域名的IP地址,一旦成功获取(或主机解析完成),浏览器就会连接服务器所在的地址。关于域名解析就不详细说了。

字节顺序

通常,计算机(CPU相关)和网络协议采用的是不同的字节顺序,计算机采用的是小端(Little-Endian)而网络协议采用大端(Big-Endian),具体可以看详解大端模式和小端模式。所以在发送请求前,我们必须对IP地址和端口进行字节顺序的转换,保证字节顺序的一致性。所以我们可以在netinet/in.h中看到这样的宏

# if __BYTE_ORDER == __BIG_ENDIAN
/* The host byte order is the same as network byte order,
   so these functions are all just identity.  */
# define ntohl(x)   __uint32_identity (x)
# define ntohs(x)   __uint16_identity (x)
# define htonl(x)   __uint32_identity (x)
# define htons(x)   __uint16_identity (x)
# else
#  if __BYTE_ORDER == __LITTLE_ENDIAN
#   define ntohl(x) __bswap_32 (x)
#   define ntohs(x) __bswap_16 (x)
#   define htonl(x) __bswap_32 (x)
#   define htons(x) __bswap_16 (x)
#  endif
# endif

这里的源码很容易看懂,n:network h:host s:short l:long,htonl()就是计算机->网络协议 long
Linux为我们提供的API如下

/* Functions to convert between host and network byte order.

   Please note that these functions normally take `unsigned long int' or
   `unsigned short int' values as arguments and also return them.  But
   this was a short-sighted decision since on different systems the types
   may have different representations but the values are always the same.  */

extern uint32_t ntohl (uint32_t __netlong) 
     __THROW __attribute__ ((__const__));
extern uint16_t ntohs (uint16_t __netshort)
     __THROW __attribute__ ((__const__));
extern uint32_t htonl (uint32_t __hostlong)
     __THROW __attribute__ ((__const__));
extern uint16_t htons (uint16_t __hostshort)
     __THROW __attribute__ ((__const__));

下面的函数则将IP地址转换为了网络协议的字节顺序

/* Convert Internet host address from numbers-and-dots notation in CP
   into binary data in network byte order.  */
extern in_addr_t inet_addr (const char *__cp) __THROW;

开启Socket

socket()

英语好的直接看这个http://man7.org/linux/man-pages/man2/socket.2.html

/* Create a new socket of type TYPE in domain DOMAIN, using
   protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
   Returns a file descriptor for the new socket, or -1 for errors.  */
extern int socket (int __domain, int __type, int __protocol) __THROW;

可以看到,socket函数创建成功以后,会返回一个文件描述符,Linux的一大哲学就是一切皆文件,socket也不例外。要创建这个socket,需要传入三个参数

  • domain :协议域,原来的想法是每个通信域(如PF_INET)可能对应多个协议(如AF_INET),而事实上支持多个协议的通信域一直没有实现。因此,在linux内核中,AF_***与PF_***被定义为同一个常数(在socket.h中可以看到),因此,在编程时可以不加区分地使用他们。 所以这个参数直接传 PF_INET就可以了
  • type :socket类型,这里只介绍SOCK_STREAM和SOCK_DGRAM
    • SOCK_STREAM:提供顺序的、可靠的、双向的、基于连接的字节流。这里我们使用TCP,所以选择此类型
    • SOCK_DGRAM:支持数据报(一个固定的最大长度的无连接、不可靠的消息)。对应UDP
  • protocol: 协议,就是IPPROTO_TCP、IPPROTO_UDP等,如果使用IPPROTO_IP,就会根据type来自动选择TCP或UDP

了解了这些信息,根据我们的需求,创建socket应该是

int mysock= socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

接下来的步骤则分为服务器端和客户端
【网络编程入门】使用socket在Linux下实现即时通信软件_第1张图片

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

bind()

创建完socket,接下来我们要为它绑定IP和端口等信息

/* Give the socket FD the local address ADDR (which is LEN bytes long).  */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
     __THROW;

参数fd就是上一步我们创建socket得到的文件描述符。

第二个参数是一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,这里我们用的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 */
};

结合前面字节顺序的知识,这里我们要创建的sockaddr应该是这样:

    struct sockaddr_in addr;
    addr.sin_family = PF_INET;
    addr.sin_port = htons(SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_IP);

参数len对应sockaddr的长度,所以要为socket绑定地址只要

bind(mysock,(struct sockaddr *)&addr,sizeof(addr))

listen()

/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
extern int listen (int __fd, int __n) __THROW;

参数fd为socket的文件描述符,参数n为允许连接的最大数量。调用listen()后,socket就会变为被动状态,等待客户端的连接请求。

连接服务器端

connect()

待更

你可能感兴趣的:(Linux,C++)