2019-10-07
关键字:
TCP 网络通信模型中通常都都采用 C/S架构。
所谓 C/S架构 即通信双方一方是客户端 Client,另一方是服务端 Server。
服务端的整体流程如下:
1、socket()
2、bind()
3、listen()
4、accept()
5、write()
6、close()
客户端的整体流程如下:
1、socket()
2、connect()
3、read()
4、close()
socket() 函数:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数执行成功时会返回一个代表着这个 socket 通信的文件描述符,失败时返回 EOF 并设置对应 errno。
参数 domain 表示通信协议类型,一般可以填以下几个值:
1、AF_INET;
IPV4
2、AF_INET6;
IPV6
3、AF_UNIX, AF_LOCAL;
本地通信。
4、AF_NETLINK;
内核与用户空间的通信,一般在做设备通信的时候会用上。
5、AF_PACKET
参数 type 指 socket 的类型,一般可以填 SOCK_STREAM 或 SOCK_DGRAM 或 SOCK_RAW。
参数 protocol 一般填 0 即可。但若 type 是 SOCK_RAW 时才需要按需填写这个参数的值。
bind() 函数:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockid, const struct sockaddr *addr, socklen_t addrlen);
执行成功时返回值 0,失败时返回 EOF 并设置对应的 errno。
参数 sockId 表示通过 socket() 函数拿到的文件描述符。
参数 addr 所指代的结构体原型为:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
但对于 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 */
};
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
所以这个参数在 IPV4 下是将上面的 sockaddr_in 强转成 sockaddr 来填入。由于两个结构体长度不一致,因此要在 sockaddr_in 末尾填充 0。
参数 addrlen 就是参数 2 的结构体的长度。
listen() 函数:
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockId, int backlog);
执行成功返回值 0,失败返回 EOF 并设置相应的 errno。
参数 sockId 是 socket 的文件描述符。
参数 backlog 一般填 5。这个参数表示同时允许多少个客户端和服务器进行三次握手的连接过程。
accept() 函数:
它的作用就是阻塞并等待客户端的连接。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
执行成功时会返回成功进行连接的 socket 的文件描述符,这是一个新的 socket 文件描述符,换句话说,当有客户端成功连接以后,与这个客户端通信所使用的 sockfd 就是这个新的 socket 文件描述符,旧的 sockfd 将继续用来等待下一个客户端的连接。函数执行失败时返回 EOF 并设置相应的 errno。
参数 addr 与 addrlen 这两个参数是用来保存连接进来的客户端的信息的。例如客户端的网络地址、端口号等信息。如果不想关心客户端的这些信息,就可以直接填 NULL 只靠新的 sockfd 来与客户端进行基础的 IO 通信。
connect() 函数:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
执行成功时返回值 0,失败时返回 EOF 并设置相应的 errno。
这几个参数与上面介绍的几个函数几乎是一样的,就不再解释了。
write()、read() 与 close() 函数:
当客户端与服务端成功建立连接以后,就可以通过对应的 sockfd 或 newfd 来进行标准 IO 通信了。说白了就是就把这个网络通信过程当成是普通文件 IO 过程就好。因此关于这里的 IO 函数就不再赘述了。
实例:
以下贴出一个最简单的实现了 tcp 通信的服务端与客户端功能的实例代码:
服务端代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <netinet/ip.h> #define IP_ADDR "192.168.77.200" int main() { int sockfd = -1; //对于 IPV4 来讲绑定时用的结构体就是这个。 struct sockaddr_in sin; // 1. 经典的创建 socket 函数调用方式。 if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(-1); } /* 由于 bind() 函数中参数 addr 所代表的通用结构体 sockaddr 的长度与 IPV4 下的 sockaddr_in 结构体不一致,因此需要将多余的数据部分清零。 */ bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; //对于 IPV4 来讲这个变量的值恒填 AF_INET。 sin.sin_port = htons(17173);//端口号,要求要网络字节序,因此需要作转换。 //sin.sin_addr.s_addr = inet_addr(IP_ADDR);//这种将字符串形式IPV4地址转换成整数形式 //的函数比较落后且有缺陷,一般不建议使用。 //可靠的 inet_pton() 转换函数 if(inet_pton(AF_INET, IP_ADDR, (void *)&sin.sin_addr.s_addr) != 1) //inet_pton() 的返回值较特殊。 { perror("inet_pton"); exit(-1); } // 2. 绑定操作。 if(bind(sockfd, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("bind"); exit(-1); } // 3. 开始监听 if(listen(sockfd, 5) < 0) { perror("listen"); exit(-1); } // 4. 等待连接 int newfd = -1; newfd = accept(sockfd, NULL, NULL); if(newfd < 0) { perror("accept"); exit(-1); } // 5. 读写 int ret = -1; char buf[64]; while(1) { bzero(buf, 64); do{ ret = read(newfd, buf, 64 - 1); }while(ret < 0 && EINTR == errno); if(ret < 0) { perror("read"); exit(-1); } if(ret == 0) { //客户端已经关闭连接。 printf("Client was exited!\n"); break; } printf("Received:%s\n", buf); } close(newfd); close(sockfd); printf("bye\n"); return 0; }View Code
客户端代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <netinet/ip.h> #define IP_ADDR "192.168.77.200" int main() { int sockfd = -1; //对于 IPV4 来讲绑定时用的结构体就是这个。 struct sockaddr_in sin; // 1. 经典的创建 socket 函数调用方式。 if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(-1); } // 2. 连接到服务端。 bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(17173); if(inet_pton(AF_INET, IP_ADDR, (void *)&sin.sin_addr.s_addr) != 1) { perror("inet_pton"); exit(-1); } if(connect(sockfd, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("connect"); exit(-1); } // 3. IO 通信 char buf[64]; while(1) { bzero(buf, 64); printf("input > "); //fflush(); fgets(buf, 63, stdin); write(sockfd, buf, strlen(buf)); } printf("bye\n"); close(sockfd); return 0; }View Code
实例代码优化:
在上面的实例代码中,服务端所绑定的 IP 地址是写死的,这不便于程序的移植运行。可以将代码修改一下,使得程序可以不必限定绑定某个具体的 IP 地址,以提高程序的移植性。
修改的方法如下,主要就是在 bind() 之前将 sockaddr_in 的参数修改一下:
bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(17173); sin.sin_addr.s_addr = htonl(INADDY_ANY);
只需要将要绑定的 IP 地址按照上述设定即可。上述的 INADDY_ANY 是一个宏,它代表的值是 -1,将绑定的 IP 地址设成 -1 就是告诉系统我要不限定 IP 地址绑定。
在上面的实例代码中,服务端是没有取客户端的信息的。若要获取客户端的信息,可以按照如下修改,主要的修改是完善 accept() 的参数。
struct sockaddr_in cin; int newfd = -1; newfd = accept(sockfd, (struct sockaddr *)&cin, sizeof(cin)); if(newfd < 0) { perror("accept"); exit(-1); } char ipv4[16]; if(!inet_ntop(AF_INET, (void *)&cin.sin_addr.s_addr, ipv4, sizeof(cin))) { perror("inet_ntop"); exit(-1); } printf("The ip of client:%s:%d\n", ipv4, ntohs(cin.sin_port));
没什么难的,若对某些函数的含义拿捏不准,直接 man 一下即可。