上一次,我们介绍了套接字的概念及简单的UDP网络程序 戳这里查看;今天,我们介绍一个简单的TCP网络程序。
一. 地址转换函数
在IPv4的socket网络编程中,sockaddr_in中的成员dtruct in_addr sin_addr表示的是32位的IP地址,但是我们通常却是用点分十进制的字符串表示。因此,我们在使用时,经常需要互相转换。
1. 字符串转in_addr的函数:
#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family, const char* strptr, void* addrptr);
2. in_addr转字符串的函数:
char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
其中,inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPV6的in6_addr,因此,函数接口是void* addptr。
3.关于inet_ntoa
inet_ntoa这个函数返回了一个char*,但是这个函数是在自己内部为我们申请内存存放结果,那么是否需要我们手动释放?
man手册上说,inet_ntoa函数,是把结果放到了静态存储区,不需要我们手动释放。因此,这个函数,在使用的时候就需要注意,如果连续调用,第二次调用时的结果会覆盖掉上一次的结果。
APUE中,明确提出inet_ntoa不是一个线程安全的函数;在多线程环境下,还是推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
简单的TCP网络程序
服务器端
1. 常用API
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- socket()打开一个网络通讯端口,就像open打开文件描述符一样,调用时报返回-1,调用成功返回文件描述符;
- 应用程序可以向像对写文件一样,使用write/read在网络上收发数据;
- 对于IPv4,第一个参数指定为AF_INET;
- 对于TCP协议,第二个参数指定为SOCK_STREAM,表示面向流的传输协议;对于UDP协议,第二个参数指定为SOC_DGRAM;
- 对于第三个参数protocol,一般指定为0即可。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 服务器所监听的网络地址和端口号通常是固定不变的,客服端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()调用成功返回0,失败返回-1;
- struct sockaddr*是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而他们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
- 对struct socket_in中的sin_addr.s_addr可以使用INADDR_ANY初始化,这个宏表示本地的任意地址,服务器可能有多个网卡,每个网卡也可能绑定多个IP,这样设置可以再所有的IP地址上监听,直到与某个客户端建立连接才确定下来
addr.sin_addr.s_addr = htonl(INADDR_ANY);
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- listen()声明sockfd处于监听状态,并且允许backlog个客户端处于等待连接状态,一般设置为5;
- 参数backlog等待队列,一般设置为5,不会太大,太长会花费资源在维护队列上;没有的话,不能使服务器随时处于满载状态;
- listen()成功返回0,失败返回-1。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- TCP三次握手后,服务器调用accept()接受连接;
- 如果调用accpet时还没有客户端的连接请求,就阻塞等到有客户端来连接;
- addr是一个传出型参数,调用后传出客户端的地址和端口号,如果传NULL,表示不关心客户端的地址;
- accept的返回值才是真正使用的套接字,最开始创建的是监听套接字。举一个例子,就像卖手机的店一样,有的工作人员在外面向店里拉客,而进来的客人是由另外的工作人员为其介绍手机。
2. 服务器端代码
服务器端的作用是接受client的请求,并将接受到的client的数据,返回给client。
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
int startup(char* ip, int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
printf("create socket error, errno is:%d, errstring is:%s\n", errno, strerror(errno));
exit(1);
}
struct sockaddr_in server_socket;
server_socket.sin_family = AF_INET;
server_socket.sin_port = htons(port);
server_socket.sin_addr.s_addr = inet_addr(ip);
if(bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in)) < 0)
{
printf("bind error, errno is:%d, errstring is:%s\n", errno, strerror(errno));
close(sock);
exit(2);
}
if(listen(sock, 5) < 0)
{
printf("listen error, error code is:%d, errstring is:%s\n", errno, strerror(errno));
close(sock);
exit(3);
}
printf("bind and listen success\n");
return sock;
}
void service(int sock, char* ip, int port)
{
while(1)
{
char buf[1024]={0};
ssize_t s = read(sock, buf, sizeof(buf));
if(s < 0)
{
perror("read");
exit(5);
}
else if(s > 0)
{
buf[s-1] = 0;
if(strcmp(buf,"quit")==0)
{
printf("client quit ....\n");
break;
}
printf("client[%s][%d]:%s\n", ip, port, buf);
write(sock, buf, sizeof(buf));
}
else //read finish
{
printf("client quit ....\n");
close(sock);
break;
}
}
printf("connect end.....Please wait.....\n");
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip][port]\n", argv[0]);
exit(4);
}
int listen_sock = startup(argv[1], atoi(argv[2]));//监听套接字
struct sockaddr_in peer;//接受客户端的地址和端口号
for(;;)
{
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);//接受连接
if(new_sock < 0)
{
printf("accept error, error code is:%d, errstring is:%s\n", errno, strerror(errno));
continue;
}
char buf[32];
memset(buf, 0, sizeof(buf));
inet_ntop(AF_INET, &peer.sin_addr, buf, sizeof(buf));
printf("connect success! ip is:%s, port is:%d\n", buf, ntohs(peer.sin_port));
service(new_sock, buf, ntohs(peer.sin_port));//服务器端执行的操作
close(new_sock);
}
close(listen_sock);
return 0;
}
客户端
1. 常用API
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 客户端不需要固定的端口号,因此不必调用bind,它的端口号由内核自动分配;
- 客户端需要调用connect(),连接服务器;成功返回0,失败返回-1;
2. 客户端代码
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<arpa/inet.h>
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage %s[ip]\n", argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in local;
local.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &local.sin_addr);
// local.sin_addr.s_addr = inet_addr(argv[1]);
local.sin_port = htons(8080);
int ret = connect(sock, (struct sockaddr*)&local, sizeof(local));
if(ret < 0)
{
printf("connect errno, error code is:%d, errstring is:%s\n", errno, strerror(errno));
exit(2);
}
printf("connect success! \n");
while(1)
{
char buf[1024]={0};
printf("client# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0)
{
buf[s-1] = 0;
write(sock, buf, sizeof(buf));
if(strcmp(buf, "quit") == 0)
{
printf("client quit\n");
break;
}
read(sock, buf, sizeof(buf));
printf("server# %s\n", buf);
}
}
close(sock);
return 0;
}
测试程序
1. 测试服务器
可以看到程序时处于监听状态的。
2.测试客户端
3.多进程与多线程
我们上面的程序中,再启动一个客户端,尝试连接服务器,发现不能连接;这是因为,我们accpet了一个请求之后,一直在循环read,没有办法继续调用accept,所以不能接受新的请求。
多进程版
1. 主函数如上图所示,startup函数与service函数与上面的程序是一样的;
2. 我们创建了一个子进程,在子进程中又创建了一个进程,我们让孙子进程执行read操作,然后让子进程立即退出,这样父进程等待子进程的时间就会很短了;而孙子进程如果退出,会被系统回收。
3. 多进程版本的特点:
a. 易于编写代码,
b. 比较稳定(进程的独立性,彼此之间不影响)
c. 服务器接accept之后,才开始创建进程,占时间;
d. 进程创建占用资源较大,同时服务的人数有限;
e. 切换进程的成本较大,影响性能。
多线程版
1. work函数为线程需要执行的程序,将sock,port和ip定义为一个结构体,通过结构体传参;
2. 多线程的特点
a. 创建成本小于进程,但也是有一定的成本;
b. 占用资源小于进程,但也是同时服务的人数也是有限的;
c. 切换成本小于进程,但也是需要成本的;
d. 多线程不稳定,如果其中一个线程挂了,整个进程都会出现问题;
e. 编写多线程程序,还需要注意线程安全的问题。