在前一篇文章中讲到了如何使用winsock:【网络编程入门】在C++中使用Windows TCP Sockets,也算是勉强入门了吧,接下来自己写一下在Linux下的网络编程,也算是把知识理一遍。代码架构参考了实验楼的C++ 实现即时通信软件
首先我们知道两个进程要通信,必须要求进程有唯一标识,本地进程的通信使用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;
英语好的直接看这个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,需要传入三个参数
了解了这些信息,根据我们的需求,创建socket应该是
int mysock= socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
创建完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))
/* 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就会变为被动状态,等待客户端的连接请求。
待更