Linux中TCP/IP通信实践,FOR C/C++
一、客户机与服务机交互流程
二、函数详解
1. socket()
- 作用:初始化,成功后返回服务器套接字文件描述符,失败返回-1
- 函数原型:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
- 参数:
(1)domain:即协议域。又称为协议族(family),AF_INET
(2)type:指定socket类型。常用的socket类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
(3)protocol:指定协议。一般设置为0。
2. bind()
- 作用:绑定地址端口,使文件描述符监听addr所描述的端口和地址
- 函数原型:
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
- 参数:
(1)sockfd:即需要绑定的服务器描述字。
(2)struct sockaddr* :通用指针类型。addr参数实际上可以接受多种协议的sockaddr结构体,该结构体存放协议族、端口和地址。
(3)addrlen:用于指定结构体sockaddr的长度。
3. listen()
- 作用:声明socket处于监听状态。最多允许有backlog个客户端处与连接等待状态,如果接受到更多的连接请求就忽略。
- 函数原型:
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd,int backlog);
- 参数
(1)sockfd:服务器描述字。
(2)backlog:最多客户机等待的数目。
4. accept()
- 作用:使socket接受客户机网络请求。与客户机三次握手连接成功后,会返回一个新的已连接socket描述字。
- 函数原型:
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:
(1)sockfd:服务器描述字。
(2)struct sockaddr* :指向struct sockaddr * ,返回客户端协议地址。
(3)addrlen:指向客户端协议地址长度。
5. connect()
- 作用:使客户端连接指定地址、端口的服务器。
- 函数原型:
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
- 参数:与bind()一致,bind()参数是自己的地址,connect()是对方(服务器)的地址。
6. write()
- 功能:write()会把参数buf所指的内存写入count个字节到参数放到所指的文件fd内。
- 返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
- 函数原型:
#include <stdio.h> #include <unistd.h> ssize_t write(int fd, const void * buf, size_t count);
7. read()
- 功能:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。
- 返回值:返回实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。
- 注意:read时fd中的数据如果小于要读取的数据,就会引起阻塞。
- 函数原型:
#include <stdio.h> #include <unistd.h> ssize_t read(int fd, void * buf, size_t count);
- read()、write()综合使用示例:
char buf[1024]; ssize_t s=read(new_sock,buf,sizeof(buf)-1); //从Client读取数据 if(s>0){ //读取成功 buf[s]=0; write(new_sock,buf,strlen(buf)); }
8. close()
- 功能:关闭当前描述字。
- 函数原型:
#include <unistd.h> int close(int __fd);
- 参数:
(1) __fd:需要关闭的描述字。
三、具体实现代码
1. 服务器端Server
(1)单进程
- 只用于一台客户机对一台服务器使用,测试可在同一台电脑两个终端进行
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h> //C++中所需要的read()、write()
static void usage(const char* proc){
printf("Usage:%s [local_ip] [local_port]\n",proc);
}
//初始化,返回服务器描述字
int startup(const char* _ip,int _port){
int sock=socket(AF_INET,SOCK_STREAM,0); //socket():初始化
if(sock<0){ //成功后返回服务器描述字,一直存在
perror("socket");
exit(2);
}
printf("fd:%d\n",sock);
struct sockaddr_in local; //用sockaddr_in存储local(Server) addr和port信息
local.sin_family=AF_INET; //协议域
local.sin_port=htons(_port); //local port
local.sin_addr.s_addr=inet_addr(_ip); //local ip
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){ //bind():绑定local(Server) port和ip到sock
perror("bind");
exit(3);
}
if(listen(sock,10)<0){ //listen():接受一个网络请求,最多10个client等待
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char *argv[]){ //用sockaddr_in存储Client addr和port信息
if(argc!=3){ //输入参数不合法
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2])); //初始化,生成服务器描述字
while(1){
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len); //接受一个Client的网络请求,成功后返回已连接的Scoket描述字
if(new_sock<0){
perror("accept");
continue;
}
printf("get a new client:[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
char buf[1024];
while(1){
ssize_t s=read(new_sock,buf,sizeof(buf)-1); //从Client读取数据
printf("Get_Size = %d\n",s);
if(s>0){ //读取成功
buf[s]=0;
printf("client# %s\n",buf);
write(new_sock,buf,strlen(buf));
}
else if(s==0){ //Client离线
printf("client quit!\n");
break;
}
else{ //读取失败
perror("read");
break;
}
}
close(new_sock); //读取失败就退出
}
}
(2)多进程
- 用于多台客户机对应一台服务器,也可在多个终端进行测试
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/wait.h> //waitpid()
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h> //C++中所需要的read()、write()
static void usage(const char* proc){
printf("Usage:%s [local_ip] [local_port]\n",proc);
}
int startup(const char* _ip,int _port){
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("socket");
exit(2);
}
printf("fd:%d\n",sock);
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
if(listen(sock,10)<0){
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char *argv[]){
if(argc!=3){
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));
while(1){
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock<0){
perror("accept");
continue;
}
printf("get a new client:[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//握手成功,创建子进程
pid_t id=fork(); //fork调用一次,返回两次,子进程返回0,父进程返回子进程ID
if(id<0){
perror("fork");
continue;
}
//子进程
else if(id==0){
if(fork()>0){
exit(0);
}
char buf[1024];
while(1){
ssize_t s=read(new_sock,buf,sizeof(buf)-1);
if(s>0){
buf[s]=0;
printf("client[%s:%d]# %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
write(new_sock,buf,strlen(buf));
}
else if(s==0){
printf("client quit!\n");
break;
}
else{
perror("read");
break;
}
}
close(new_sock);
exit(0);
}
//父进程
else{
close(new_sock);
waitpid(id,NULL,0);//暂时停止父进程执行,处理僵尸进程,但并不会阻塞
}
}
}
/*多进程tcp测试可以通过两个主机连接一个局域网完成。
切换到root用户,关闭自己和对方主机的防火墙—-service iptables stop
检查是否有sshd—-ps aux | grep ssh
使用命令查看IP地址是否在一个局域网内—-ifconfig
与对方主机ping一下看能不能通—-ping 192.xxx.xxx.xxx
将tcp_client执行文件发送到对方主机—-scp tcp_client 192.xxx.xxx.xxx:/home
运行tcp_server,对方主机在/home目录下运行tcp_client
*/
(3)多线程
- 同多进程,一天服务器连接多台客户机,但相对于多进程而言更稳定,因为线程相对于进程是各自私有资源,各自独立
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h> //C++中所需要的read()、write()
static void usage(const char* proc){
printf("Usage:%s [local_ip] [local_port]\n",proc);
}
int startup(const char* _ip,int _port){
//初始化
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("socket");
exit(2);
}
printf("fd:%d\n",sock);
//绑定ip,port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
//监听
if(listen(sock,10)<0){
perror("listen");
exit(4);
}
return sock;
}
void *request(void *arg){
int new_sock=(int)arg;
char buf[1024];
while(1){
ssize_t s=read(new_sock,buf,sizeof(buf)-1);
if(s>0){
buf[s]=0;
printf("client# %s\n",buf);
write(new_sock,buf,strlen(buf));
}
else if(s==0){
printf("client quit!\n");
break;
}
else{
perror("read");
break;
}
}
close(new_sock);
return (void*)0;
}
int main(int argc,char *argv[]){
if(argc!=3){
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));
while(1){
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock<0){
perror("accept");
continue;
}
printf("get a new client:[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//一旦三次握手成功就开启新线程
pthread_t id;
pthread_create(&id,NULL,request,(void*)new_sock);
pthread_detach(id);
}
}
/*多线程tcp测试可以通过两个主机连接一个局域网完成。
切换到root用户,关闭自己和对方主机的防火墙—-service iptables stop
检查是否有sshd—-ps aux | grep ssh
使用命令查看IP地址是否在一个局域网内—-ifconfig
与对方主机ping一下看能不能通—-ping 192.xxx.xxx.xxx
将tcp_client执行文件发送到对方主机—-scp tcp_client 192.xxx.xxx.xxx:/home
运行tcp_server,对方主机在/home目录下运行tcp_client
*/
2. 客户端Client
单进程、多进程、多线程客户端均如下所示:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h> //C++中所需要的read()、write()
static void usage(const char* proc){
printf("Usage:%s [local_ip] [local_port]\n",proc);
}
int main(int argc,char* argv[]){
if(argc!=3){
usage(argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("socket");
return(2);
}
struct sockaddr_in peer;
peer.sin_family=AF_INET;
peer.sin_port=htons(atoi(argv[2]));
peer.sin_addr.s_addr=inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&peer,sizeof(peer))<0){
perror("connect");
return 3;
}
char buf[1024];
while(1){
printf("Please Enter# ");
fflush(stdout); //清空输出缓存区
fflush(stdin);
ssize_t s = read(0,buf,sizeof(buf)-1); //读取用户输入
printf("Read_Size = %d\n",s);
if(s>0){
buf[s-1]=0;
if(strlen(buf) == 0)
continue;
write(sock,buf,strlen(buf)); //向服务器写数据
//读取服务器回调数据并显示
ssize_t _s=read(sock,buf,sizeof(buf)-1);
if(_s>0){
buf[_s]=0;
printf("server echo# %s\n",buf);
}
else if(s==0){
printf("server quit!\n");
break;
}
}
}
close(sock);
return 0;
}
– 感谢https://blog.csdn.net/akiqiu/article/details/75098177中提供的代码,为自己学习提供了很大帮助,上述代码基于此修改调试至更优。
四、多进程、多线程知识补充
1. 多线程与多进程
- 进程:操作系统中资源分配的最小单位
- 线程:CPU调度的最小单位
2. 进程标识符
- 每个进程都有一个非负整数表示唯一进程ID
3. 多进程基本函数
(1)fork():
- 函数原型:
#include<unistd.h> pid_t fork(void);
- fork()调用一次,返回两次:
- 子进程:return 0
- 父进程:return 新子进程ID
- 返回负数:错误
(2)waitpid():
- 功能:暂时停止目前进程的执行,直到有信号来或者子进程结束,用于处理僵尸进程,但可以调节参数使之不阻塞父进程。
- 示例:
waitpid(id,NULL,0);//暂时停止父进程执行,处理僵尸进程,但并不会阻塞