一、TCP socket API 详解
头文件#include<sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器) int socket(int domain, int type, int protocol); 参数: domian:采用的是哪一个协议,AF_INET(IPV4)经常使用的,AF_INET6(IPV6) type采用地套接字类别,sock_streaM(大写 流式套接字),sock_dgran(大写 用户数据报套接字) protocol默认为0.
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符; 应用程序可以像读写文件一样用read/write在网络上收发数据; 如果socket()调用出错则返回-1; 对于IPv4, family参数指定为AF_INET; 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议 protocol参数的介绍从略,指定为0即可。
// 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address,socklen_t address_len); 参数: socket打开网络文件的文件描述符 struct socketaddr:参数通常是struct socketaddr_in类型里边包括IP地址和端口号 address_len:传入address对应的长度。
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号; bind()成功返回0,失败返回-1。 bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号; 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度; 之前在写UDP套接字是如下赋值的:
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(post);
in_addr.sin_addr.s_addr=INADDR_ANY;
该赋值方式还有如下方式:
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
- 端口号为SERV_PORT, 我们定义为9999;
int listen(int sockfd,int backlog); 参数: sock要监听的文件描述符号 backlog:当底层连接比较多而无法处理的底层连接的数量(全连接队列)
在tcp套接字通信时在绑定后,必须将套接字设置为监听状态,允许客户端在任何时候来连接sever,因为tcp通信是需要建立连接的。
int accept(int sockfd,struct sockaddraddr,socklen_taddrlen); 参数 sockfd套接字 struct sockaddraddr,socklen_taddrlen是一个输出型参数包含有源主机的IP地址和源端口port
返回值是一个文件描述符 这个文件描述符和socket()返回值的文件描述符有什么区别? 举一个例子: 当去一家餐馆吃饭时,当进入这家餐馆,门口有位服务员把你引到包间他的任务就结束了,他就继续去门口看有没有顾客来(这个服务员就像socket()返回值被设为监听状态一直检测有没有链接到来只有一个)于是来了另一位服务员来给你菜单,端菜,伺候你(这就像accept的返回值的文件描述符可以有多个),拉客人的服务员只有一个,负责点餐的有多个 socket()返回值负责从底层获取链接,accept负责通信任务
三次握手完成后, 服务器调用accept()接受连接; 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来; addr是一个传出参数,accept()返回时传出客户端的地址和端口号; 如果给addr 参数传NULL,表示不关心客户端的地址; addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
客户端需要调用connect()连接服务器; connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址; connect()成功返回0,出错返回-1;
单进程TCP套接字通信程序:
客户端程序
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<string>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;
class tcpsever{
private:
string ip;
int port;
int sock;
public:
tcpsever(string ip_="127.0.0.1",int port_=9999):
ip(ip_),port(port_)
{
}
void Init(){
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
cout<<"创建失败"<<endl;
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(port);
in_addr.sin_addr.s_addr=inet_addr(ip.c_str());
if(connect(sock,(struct sockaddr*)&in_addr,sizeof(in_addr))!=0){
cout<<"链接建立失败"<<endl;
exit(2);
}
}
void star(){
char buf[64];
while(1){
string sin;
cout<<"请输入请求#";
fflush(stdout);
size_t ss=read(0,buf,strlen(buf)-1);
if(ss>0){
send(sock,buf,sizeof(buf),0);
char buff[64];
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s-1]=0;
cout<<buff<<endl;
}
}
}
}
~tcpsever(){
close(sock);
}
};
int main(int argc,char*argv[]){
tcpsever tc(argv[1],atoi(argv[2]));
tc.Init();
tc.star();
return 0;
}
服务器程序
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<string>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;
class tcpsever{
private:
string ip;
int port;
int sock;
public:
tcpsever(string ip_="127.0.0.1",int port_=9999):
ip(ip_),port(port_)
{
}
void Init(){
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
cout<<"创建失败"<<endl;
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(port);
in_addr.sin_addr.s_addr=inet_addr(ip.c_str());
if(connect(sock,(struct sockaddr*)&in_addr,sizeof(in_addr))!=0){
cout<<"链接建立失败"<<endl;
exit(2);
}
}
void star(){
char buf[64];
while(1){
string sin;
cout<<"请输入请求#";
fflush(stdout);
size_t ss=read(0,buf,strlen(buf)-1);
if(ss>0){
send(sock,buf,sizeof(buf),0);
char buff[64];
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s-1]=0;
cout<<buff<<endl;
}
}
}
}
~tcpsever(){
close(sock);
}
};
int main(int argc,char*argv[]){
tcpsever tc(argv[1],atoi(argv[2]));
tc.Init();
tc.star();
return 0;
}
说明:
当客户端退出时,为什么再一次重新链接时会链接不上? 因为当服务器获取链接后进入到service()时也是死循环,在不断的读写。应该是当一个客户端退出连接时,服务器应该知道客户端退出了并且退出当前服务,还要关闭这个链接对应新打开的套接字。刚才当客户端退出了再次连接时连接不上是因为服务器不知道客户端推出了。看对应recv的返回值当大于0是读取成功,当等于0时代表对方关闭了,这个管道相似,当写段关闭且将写文件描述符关闭,读端将缓冲区内数据区内读完,再度就会读到0值 TCP和UDP套接字的服务端都不需要绑定端口号原因相似,因为在绑定端口号后,可能别的端口号也要用这个端口号,会造成冲突,很容易造成客户端启动失败。解决方式就是让操作系统自动分配端口号。
多进程版的TCP套接字通信
服务器端程序
#include<iostream>
#include<sstream>
#include<signal.h>
#include<stdlib.h>
#include<string>
#include<cstdio>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;
#define num 5
class tcpsever{
private:
int port;
int listen_sock;
public:
tcpsever(int port_=9999){
port=port_;
}
void Init(){
listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0){
cout<<"创建失败"<<endl;
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(port);
in_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listen_sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
cout<<"绑定失败1"<<endl;
exit(3);
}
if(listen(listen_sock,num)<0){
cout<<"绑定失败2"<<endl;
exit(4);
}
}
void star(){
signal(SIGCHLD,SIG_IGN);
struct sockaddr_in addr;
socklen_t len=sizeof(addr);
while(1){
int sock=accept(listen_sock,(struct sockaddr*)&addr,&len);
if(sock<0){
cout<<"链接获取失败"<<sock<<endl;
continue;
}
char buf[16];
sprintf(buf,"%d",ntohs(addr.sin_port));
string str="";
str=inet_ntoa(addr.sin_addr);
str+=":";
string str2="";
stringstream str1;
str1<<ntohs(addr.sin_port);
str1>>str2;
str+=str2;
cout<<"get a new link..."<<str<<endl;
pid_t p;
size_t s=fork();
if(s==0){
close(listen_sock);
service(sock);
exit(0);
}
close(sock);
}
}
void service(int sock){
while(1){
char buff[64];
struct sockaddr_in add;
socklen_t len=sizeof(add);
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s]=0;
cout<<buff<<endl;
string str="sever have...";
send(sock,str.c_str(),sizeof(str)-1,0);
}else if(s==0){
cout<<"client have quited...."<<endl;
break;
close(sock);
}else if(s<0){
cout<<"文件读取失败"<<endl;
break;
}
}
close(sock);
}
};
int main(int argc,char*argv[]){
tcpsever tc(atoi(argv[1]));
tc.Init();
tc.star();
return 0;
}
因为在创建子进程时是按照父进程为模板创建的,父进程的大多数数据结构都和子进程相同,因为多进程版子进程只负责服务这块不需要监听,所以可以将监听套接字关闭,而父进程只负责监听获取链接,可以将sock套接字关闭,因为不关闭套接字将持续减少后续可能不够用。 size_t s=fork(); if(s==0){ close(listen_sock);子进程关闭自己的监听套接字 service(sock); exit(0); } close(sock);父进程关闭自己的sock套接字
多线程版TCP套接字程序
服务器程序
#include<iostream>
#include<sstream>
#include<pthread.h>
#include<signal.h>
#include<stdlib.h>
#include<string>
#include<cstdio>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;
#define num 5
class tcpsever{
private:
int port;
int listen_sock;
public:
tcpsever(int port_=9999){
port=port_;
}
void Init(){
listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0){
cout<<"创建失败"<<endl;
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(port);
in_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listen_sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
cout<<"绑定失败1"<<endl;
exit(3);
}
if(listen(listen_sock,num)<0){
cout<<"绑定失败2"<<endl;
exit(4);
}
}
void star(){
signal(SIGCHLD,SIG_IGN);
struct sockaddr_in addr;
socklen_t len=sizeof(addr);
while(1){
int sock=accept(listen_sock,(struct sockaddr*)&addr,&len);
if(sock<0){
cout<<"链接获取失败"<<sock<<endl;
continue;
}
char buf[16];
sprintf(buf,"%d",ntohs(addr.sin_port));
string str="";
str=inet_ntoa(addr.sin_addr);
str+=":";
string str2="";
stringstream str1;
str1<<ntohs(addr.sin_port);
str1>>str2;
str+=str2;
cout<<"get a new link..."<<str<<endl;
pthread_t p;
int *pp=new int(sock);
pthread_create(&p,NULL,PthreadService,(void*)&pp);
}}
static void service(int sock){
while(1){
char buff[64];
struct sockaddr_in add;
socklen_t len=sizeof(add);
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s]=0;
cout<<buff<<endl;
string str="sever have...";
send(sock,str.c_str(),sizeof(str)-1,0);
}else if(s==0){
cout<<"client have quited...."<<endl;
break;
close(sock);
}else if(s<0){
cout<<"文件读取失败"<<endl;
break;
}
}
}
static void *PthreadService(void *rid){
pthread_detach(pthread_self());
int *p=(int *)rid;
int sock=*p;
service(sock);
delete p;
}
};
int main(int argc,char*argv[]){
tcpsever tc(atoi(argv[1]));
tc.Init();
tc.star();
return 0;
}
int pp=new int(sock); pthread_create(&p,NULL,PthreadService,(void)pp);为什么在创建线程时要重新开辟一段空间拷贝sock呢? 因为在创建线程时如果传sock就必须将sock地址传入,因为线程之间共享资源,当有一个线程没有服务完,在重新当在一次获取链接时会改变sock值,所以必须将sock保存下来。这个方法让sock变成线程私有的。
总结:
- 单进程版本:不可使用
- 多进程版本:健壮性,比较吃资源,效率低下
- 多线程版本:健壮性不强,交吃资源,效率相对较高
大量客户端:系统会造成大量执行流!切换有可能效率低下的主要原因。 - 引入线程池版本。
线程池版的TCP在线翻译程序
线程池
#include<iostream>
#include<string.h>
#include<sstream>
#include<pthread.h>
#include<signal.h>
#include<stdlib.h>
#include<string>
#include<map>
#include<cstdio>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<queue>
using namespace std;
class test {
private:
int sock;
map<string,string> mp;
public:
test() {}
test(int a) :sock(a) {
mp.insert(pair<string,string>("苹果","apple"));
mp.insert(pair<string,string>("apple","苹果"));
mp.insert(pair<string,string>("香蕉","banana"));
mp.insert(pair<string,string>("红色","red"));
mp.insert(pair<string,string>("黑色","block"));
mp.insert(pair<string,string>("蓝色","blue"));
mp.insert(pair<string,string>("绿色","green"));
}
void run(){
while(1){
char buff[1024];
struct sockaddr_in add;
socklen_t len=sizeof(add);
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s-1]=0;
string str="";
string s1=buff;
send(sock,mp[s1].c_str(), mp[s1].size(), 0);
}else if(s==0){
cout<<"client have quited...."<<endl;
close(sock);
break;
}else if(s<0){
cout<<"文件读取失败"<<endl;
break;
}
}
}
~test() {
close(sock);
}
};
class ThreadPool {
private:
queue<test*> q;
int cap;
pthread_mutex_t lock;
pthread_cond_t cond;
bool quit;
public:
bool isempty() {
return q.size() == 0;
}
ThreadPool(int cap_=5) :cap(cap_), quit(false)
{}
void Init() {
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
pthread_t t;
for (int i = 0; i < cap; i++) {
pthread_create(&t, NULL, thread_running, (void*)this);
}
}
void Lock() {
pthread_mutex_lock(&lock);
}
void UnLock() {
pthread_mutex_unlock(&lock);
}
void Waitcond() {
pthread_cond_wait(&cond, &lock);
}
void threadswakeup() {
pthread_cond_broadcast(&cond);
}
bool Quit() {
return quit;
}
static void* thread_running(void* rid) {
ThreadPool* this_d = (ThreadPool*)rid;
while (!this_d->Quit()) {
this_d->Lock();
while (!this_d->Quit() && this_d->isempty()) {
this_d->Waitcond();
}
test* t;
if (!this_d->Quit() && !this_d->isempty()) {
this_d->out(&t);
}
t->run();
this_d->UnLock();
delete t;
}
}
public:
void put(test&t) {
pthread_mutex_lock(&lock);
q.push(&t);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond);;
}
void threadquit() {
if (!isempty()) {
return;
}
quit = true;
threadswakeup();
}
void out(test**t) {
test* a = q.front();
*t = a;
q.pop();
}
~ThreadPool() {
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
void service(int sock){
while(1){
char buff[64];
struct sockaddr_in add;
socklen_t len=sizeof(add);
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s]=0;
cout<<buff<<endl;
string str="sever have...";
send(sock,str.c_str(),sizeof(str)-1,0);
}else if(s==0){
cout<<"client have quited...."<<endl;
break;
close(sock);
}else if(s<0){
cout<<"文件读取失败"<<endl;
break;
}
}
}
};
服务器端程序
#include<iostream>
#include<sstream>
#include<pthread.h>
#include<signal.h>
#include<stdlib.h>
#include<string>
#include<stdio.h>
#include<string.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
#include "pthreadpool.hpp"
using namespace std;
#define num 5
class tcpsever{
private:
int port;
int listen_sock;
ThreadPool *tp;
public:
tcpsever(int port_=9999):
tp(NULL)
{
port=port_;
}
void Init(){
listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0){
cout<<"创建失败"<<endl;
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(port);
in_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listen_sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
cout<<"绑定失败1"<<endl;
exit(3);
}
if(listen(listen_sock,num)<0){
cout<<"绑定失败2"<<endl;
exit(4);
}
}
void star(){
signal(SIGCHLD,SIG_IGN);
struct sockaddr_in addr;
socklen_t len=sizeof(addr);
while(1){
int sock=accept(listen_sock,(struct sockaddr*)&addr,&len);
if(sock<0){
cout<<"链接获取失败"<<sock<<endl;
continue;
}
char buf[16];
sprintf(buf,"%d",ntohs(addr.sin_port));
string str="";
str=inet_ntoa(addr.sin_addr);
str+=":";
string str2="";
stringstream str1;
str1<<ntohs(addr.sin_port);
str1>>str2;
str+=str2;
cout<<"get a new link..."<<str<<endl;
tp=new ThreadPool();
tp->Init();
test* p1=new test(sock);
tp->put(*p1);
}
}
static void service(int sock){
while(1){
char buff[1024];
struct sockaddr_in add;
socklen_t len=sizeof(add);
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s]=0;
cout<<buff<<endl;
string str="sever have...";
send(sock,buff,strlen(buff),0);
}else if(s==0){
cout<<"client have quited...."<<endl;
break;
close(sock);
}else if(s<0){
cout<<"文件读取失败"<<endl;
break;
}
}
}
static void *PthreadService(void *rid){
pthread_detach(pthread_self());
int *p=(int *)rid;
int sock=*p;
service(sock);
delete p;
}
};
int main(int argc,char*argv[]){
tcpsever tc(atoi(argv[1]));
tc.Init();
tc.star();
return 0;
}
客户端
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<string>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;
class tcpsever{
private:
string ip;
int port;
int sock;
public:
tcpsever(string ip_="127.0.0.1",int port_=9999):
ip(ip_),port(port_)
{
}
void Init(){
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
cout<<"创建失败"<<endl;
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(port);
in_addr.sin_addr.s_addr=inet_addr(ip.c_str());
if(connect(sock,(struct sockaddr*)&in_addr,sizeof(in_addr))!=0){
cout<<"链接建立失败"<<endl;
exit(2);
}
}
void star(){
char buf[64];
while(1){
string sin;
cout<<"请输入请求#";
cin>>buf;
fflush(stdout);
send(sock,buf,sizeof(buf),0);
char buff[1024];
size_t s=recv(sock,buff,sizeof(buff)-1,0);
if(s>0){
buff[s]=0;
cout<<"翻译结果:"<<buff<<endl;
}else if(s==0){
break;
}
}
}
~tcpsever(){
close(sock);
}
};
int main(int argc,char*argv[]){
tcpsever tc(argv[1],atoi(argv[2]));
tc.Init();
tc.star();
return 0;
}
TCP协议通信流程
假如入一个服务器在一段时间内有大量服务器来连接他,服务器就要提供链接服务,当系统有足够多的连接时就必须将连接管理起来,所谓的面向连接就是双方在连接成功后,都要在系统级别创建用于维持用于链接的数据结构用于保存链接相应的数据。创建数据结构是需要时间和空间,所以连接时需要成本的。所以udp通信比tcp通信效率更高。作为服务器是被动链接的,作为服务器是主动发起请求的。
服务器初始化:
调用socket, 创建文件描述符; 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败; 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备; 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
调用socket, 创建文件描述符; 调用connect, 向服务器发起连接请求; connect会发出SYN段并阻塞等待服务器应答; (第一次) 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手; 数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据; 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待; 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答; 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求; 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次); 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次); read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次) 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
|