文章目录
- 一、TCP套接字接口
- (一)inet_aton (和inet_addr一样,换一种方式而已)
- (二)socket()
- (三)bind()
- (四)listen()
- (五)accept()
- (六)connet()
- 二、TCP套接字编程
- (一)注意事项
- 1.因为TCP是流式类型套接字,所以利用read和write进行读取和写入数据。
- 2.客户端注意
- 3.工作流程
- (1)TCP服务器的工作
- (2)TCP客户端的工作
- (二)原始版本
- (三)封装为多进程,多线程
- 1.多进程版本1
- 2.多进程版2
- 3.多线程版
- 4.线程池
- 三、完整代码
- (一)tcpServer.hpp
- (二)tcpServer.cpp
- (三)tcpClient.hpp
- (四)tcpClient.cpp
- (五)task.hpp
查看TCP网络服务器情况和端口使用情况
netstat -nltp
一、TCP套接字接口
(一)inet_aton (和inet_addr一样,换一种方式而已)
int inet_aton(const char *cp, struct in_addr *inp);(address to net 本地字符串风格IP转网络4字节IP)cp:字符串风格IP地址。inp:转换后的存到inp中。
返回值:成功返回1;失败返回0;
注意:
(1)这类函数在转变IP风格时都会自动进行主机字节序和网络字节序之间的转换。
(2)ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
如果手动传./serverTcpd 8080 0,IP地址传的是0,此时inet_aton(0, &local.sin_addr) 即inet_aton第一个参数传的就是0,把值为0的本地字符串风格IP转网络4字节IP存入local.sin_addr中;因为INADDR_ANY 这个宏的值就是0,0是字符串风格还是网络风格无所谓,所以就等价于:inet_aton(INADDR_ANY, &local.sin_addr));,也就等价于local.sin_addr.s_addr = INADDR_ANY;
总结:inet_aton(ip_.c_str(), &local.sin_addr) ip_为0时,inet_aton(0, &local.sin_addr) 等于 local.sin_addr.s_addr = INADDR_ANY。
(二)socket()
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;如果socket()调用出错则返回-1;应用程序可以像读写文件一样用read/write在网络上收发数据;
domain:对于IPv4, family参数指定为AF_INET;
type:对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
protocol:protocol参数的介绍从略,指定为0即可
(三)bind()
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;
服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听,myaddr所描述的地址和端口号;bind()成功返回0,失败返回-1。前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
我们的程序中对myaddr参数是这样初始化的:
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
- 端口号为SERV_PORT, 我们定义为8080;
(四)listen()
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog
个客户端处于连接等待状态,如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5);
listen()成功返回0,失败返回-1。
(五)accept()
NAME
accept, accept4 - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);
三次握手完成后,服务器调用accept()接受连接;如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
addr是一个传出参数,accept()返回时传出客户端的地址和端口号;如果给addr 参数传NULL,表示不关心客户端的地址;
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的,
缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
(六)connet()
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
客户端需要调用connect()连接服务器;
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
connect()成功返回0,出错返回-1
二、TCP套接字编程
(一)注意事项
1.因为TCP是流式类型套接字,所以利用read和write进行读取和写入数据。
2.客户端注意
①客户端操作系统会自动bind,但是不需要自己显式的bind。
②需要listen吗?不需要的!监听本身就是等待别的客户端去连接你,所以客户端本身不需要设置监听状态,因为没人去连你的客户端。
③需要accept吗?不需要的,都无法设置监听,就更不需要获取连接。
3.工作流程
(1)TCP服务器的工作
- 创建socket
- 填充服务器信息struct sockaddr_in
- 将套接字和sockaddr_in 绑定bind
- socket设置为监听状态
- accept获取链接并获取客户端IP和port
- 提供服务,读取内容后完成转换写回
(2)TCP客户端的工作
- 创建socket
- 填充服务器信息struct sockaddr_in,connect向服务器发起链接请求
- 写入数据后读出服务器转化的数据
(二)原始版本
只能给一个客户提供服务,当为一个客户提供服务进入transService后,transService是死循环,除非提供完毕,否则函数不返回,则主执行流无法继续为其他客户提供服务!
void start() {
logMessage(NORMAL,"Thread init success\n");
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
if (sock < 0) {
logMessage(ERROR, "accept error, next");
continue;
}
serviceIO(sock);
close(sock); //对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
}
}
(三)封装为多进程,多线程
我们设置了对应的任务是死循环,那么线程池提供服务,就显得有不太合适。我们给线程池抛入的任务都是短任务。
因为他并没有访问任何类内成员,所以可以把执行方法提到类外,执行任务时就1.可以不用把服务和类绑定了bind(&ServerTcp::transService, this)。2.threadRoutine也没用了
我们封装服务为多进程,多线程版本的目的:多个客户端访问时,让第一个客户端访服务器时,服务器上通过子进程为客户端提供服务,然后父进程就可以继续while循环,进行下一次阻塞式获取下一个客户端的链接并为他提供服务,是并发是进行的。
小提示:为什么不用waitpid()waitpid(); 默认是阻塞等待!我们本身就是追求多进程并发,阻塞相当于还是串行了,所以我们不能用waitpid()。那WNOHANG可以吗?
——答:可以是可以,但是很麻烦,需要把各个子进程的pid保存进一个vector中,每次非阻塞等待需要轮询检测子进程pid看子进程是否退出,很麻烦,我们不选择这种方法。
1.多进程版本1
利用signal(SIGCHLD, SIG_IGN); 父进程调用signal/sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
注意:①父进程打开的文件会被子进程继承,所以子进程中本身用不到“接客”的listenSock_,所以建议关掉此文件描述符。close(listenSock_); //建议(类似管道关闭不需要的读写端一样)
②父进程accept创建的提供服务的文件描述符serviceSock就是让子进程继承使用的,那么子进程已经继承serviceSock后,父进程就用不到了,就需要关闭父进程对应的serviceSock。close(serviceSock); //这一步是一定要做的!(类似管道关闭不需要的读写端一样)。
void start() {
logMessage(NORMAL,"Thread init success\n");
signal(SIGCHLD, SIG_IGN);
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
if (sock < 0) {
logMessage(ERROR, "accept error, next");
continue;
}
pit_t id = fork();
if (id == 0) {
close(_listensock);
serverIO(sock);
close(sock);
exit(0);
}
close(sock); //对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
// father
pit_t res = waitpid(id,nullptr,0); // 阻塞等待子进程退出
if (ret > 0) {
std::cout << "waitsuccess: " << ret << std::endl;
}
}
}
2.多进程版2
3.多线程版
利用多线程去服务客户,首先创造一个ThreadData类,方便函数方法调用transService传参。
static void *threadRoutine(void *args) {
pthread_detach(pthread_self()); //设置线程分离
ThreadData *td = static_cast<ThreadData*>(args);
td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
delete td;
return nullptr;
}
void start() {
logMessage(NORMAL,"Thread init success\n");
signal(SIGCHLD, SIG_IGN);
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
if (sock < 0) {
logMessage(ERROR, "accept error, next");
continue;
}
pthread_t tid;
ThreadData* td = new ThreadData(this, sock);
// 这里不需要进行关闭文件描述符吗??不需要啦
// 多线程是会共享文件描述符表的!
pthread_create(&tid,nullptr, threadRoutine, td);
pthread_join(tid, nullptr);
}
}
4.线程池
void start() {
logMessage(NORMAL,"Thread init success\n");
signal(SIGCHLD, SIG_IGN);
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
if (sock < 0) {
logMessage(ERROR, "accept error, next");
continue;
}
ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));
}
}