TCP套接字通信

时间:2022-10-14 11:03:18

TCP套接字通信

网络中进程之间如何通信?

我们知道在本地进程间通信有很多种方式:比如管道、消息队列、共享内存、同步与互斥等,这些方法都要求通信的两个进程位于同一个主机那么网络之间该如何通信呢?在本地可以用进程PID来唯一的标识一个进程,但是在网络中是不行的,通过以前所学TCP/IP等知识,我们清楚网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“端口号”又可以唯一标识主机中的应用程序。这样,我们用ip地址和端口号组合就可唯一标识网络中两个进程,从而实现通信。

套接字的概念?

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。简单理解,套接字就是ip地址加端口号。

网络字节序

网络中要实现通信,少不了数据的传输。所以这里就引入了网络字节序的概念。在前边的学习中,我们接触到大端和小端的概念。小端:数据的低位在低地址,高位在高地址;大端:数据的低位在高地址,高位在低地址。网络数据流同样也有大端和小端之分。网络数据流先发出的是低地址,后发出的是高地址。TCP/IP规定,网络数据流采用大端字节序,即就是低位在高地址。我们之所以会说到大端和小端?是因为,网络通信的时候必须知道端口号,如果发送端是大端字节序,接收端是小端字节序,那么最后看到的端口号就是不正确的端口号,所以,我们必须将端口号在发送端和接收端之间转换成统一的字节序形式。

转化接口:

TCP套接字通信

(一)单进程的套接字通信

a、调用socket函数创建套接字

int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。

protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

b、绑定本地和端口:bind()函数

bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数的三个参数分别为:

sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如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 */

};

addrlen:对应的是地址的长度。

c、listen()、connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

d、accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

read读取socket中的数据。通信完成后,调用close关闭套接字。

代码实现:

//server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int StartUp(int port,const char* ip)
{
int ListenSock = socket(AF_INET,SOCK_STREAM,0);
if(ListenSock < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(ListenSock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(2);
}
if(listen(ListenSock,5) < 0)
{
perror("listen");
exit(3);
}
return ListenSock;
}

int main(int argc,const char* argv[])
{
if(argc != 3)
{
printf("input error\n");
return 1;
}
int len;
int listenSock = StartUp(atoi(argv[2]),argv[1]);
struct sockaddr_in client;
while(1)
{
int sock = accept(listenSock,(struct sockaddr*)&client,&len);//获取客户机的信息
if(sock < 0)
{
perror("accept");
continue;
}
printf("get a client,ip is %s,port is %d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
char buf[1024];
while(1)
{
ssize_t s = read(sock,buf,sizeof(buf)-1);//服务器进行读数据
if(s > 0)
{
buf[s] = 0;
printf("client# %s\n",buf);
}
else
{
//数据已经读完了,客户端不发送数据了
printf("client is quit!\n");
}
}
close(sock);
}
return 0;
}

//client.c
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>

int main(int argc,const char* argv[])
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
char buf[1024];
while(1)
{
printf("send# ");
fflush(stdout);
//从标准输入读数据,读到buf中,然后从buf写到管道
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s < 0)
{
perror("read");
return 3;
}
buf[s-1] = 0;
write(sock,buf,s);
}
close(sock);
return 0;
}

TCP套接字通信

(二)多进程套接字通信

上边的代码,只可以一个客户端进行发送数据,而在实际的应用中,,都是会出现多个客户端给服务器发送数据,所以,上边的实现并不实用。所以,我们可以实现一个多进程的socket通信,以实现多个客户端给服务器发数据。

实现方法:服务器端可以创建多个子进程去处理客户端发来的信息。当每次收到一个新的客户端的连接请求的时候,我们就会fork()出一个子进程,父进程用于等待子进程,子进程用于执行 读客户端发的数据 的操作。细心的你可能会发现,我们在子进程读取信息之前还进行了一次fork(),这是为什么呢?其实,我们用子进程fork()出一个孙子进程,终止掉儿子进程,儿子进程被它的父进程回收,此时的孙子进程就是一个孤儿进程,被1号进程领养。这样做的目的就是,不要让儿子进程等待孙子进程太久而消耗太多的系统资源。

代码实现:

//server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int StartUp(int port,const char* ip)
{
int ListenSock = socket(AF_INET,SOCK_STREAM,0);
if(ListenSock < 0)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(ListenSock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(ListenSock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(2);
}
if(listen(ListenSock,5) < 0)
{
perror("listen");
exit(3);
}
return ListenSock;
}

int main(int argc,const char* argv[])
{
if(argc != 3)
{
printf("input error\n");
return 1;
}
int len;
int listenSock = StartUp(atoi(argv[2]),argv[1]);
struct sockaddr_in client;
while(1)
{
int sock = accept(listenSock,(struct sockaddr*)&client,&len);//获取客户机的信息
if(sock < 0)
{
perror("accept");
continue;
}

printf("get a client,ip is %s,port is %d\n",inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
int id = fork();
if(id > 0)
{
close(sock);
while(waitpid(-1,NULL,WNOHANG) > 0);
continue;
}
else
{
close(listenSock);
if(fork() > 0)
{
exit(0);
}
char buf[1024];
while(1)
{
ssize_t s = read(sock,buf,sizeof(buf)-1);//服务器进行读数据
if(s > 0)
{
buf[s] = 0;
printf("client# %s\n",buf);
}
else
{
//数据已经读完了,客户端不发送数据了
printf("client is quit!\n");
break;
}
}
close(sock);
// exit(4);
break;
}
}
return 0;
}
//client.c
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>

int main(int argc,const char* argv[])
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
char buf[1024];
while(1)
{
printf("send# ");
fflush(stdout);
//从标准输入读数据,读到buf中,然后从buf写到管道
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s < 0)
{
perror("read");
break;
}
buf[s-1] = 0;
write(sock,buf,s);
}
close(sock);
return 0;
}

(三)多线程的套接字通信

在前边的系统编程的学习中,我们知道线程是进程内部的一个执行流,是在进程的地址空间中运行。而进程是程序的一次动态的执行过程。系统中的进程数过于多的话,会增加系统的负担。所以这里采用线程实现通信。

实现方法:

主线程中创建出一个新线程,新线程的执行函数是读取信息。类似于上边的多进程间的通信,我们可以将新的线程进行分离,分离之后的线程就不需要主线程去等待,而是由操作系统区回收。(这里我们不可以join新线程,如果这样做的话,主线程还是需要花费很长的时间去等待,所以,新的线程还是由系统去回收)

代码实现:

 //server.c
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<sys/socket.h>
int StartUp(int port,const char* ip)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
int opt = 1;
setsockopt(ListenSock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
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,5) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
void* thread_hander(void* arg)
{
int sock = *((int*)arg);
char buf[1024];
while(1)
{
ssize_t _s = read(sock,buf,sizeof(buf)-1);
if(_s > 0)
{
buf[_s-1] = 0;
printf("client say#%s\n",buf);
if(write(sock,buf,sizeof(buf)-1)<0)
{
break;
}
}
else if(_s == 0)
{
printf("client is quit!\n");
break;
}
else
{
perror("read");
break;
}
}
close(sock);
}
int main(int argc,const char* argv[])
{
if(argc != 3)
{
printf("input error\n");
return 1;
}
int listenSock = StartUp(atoi(argv[2]),argv[1]);
struct sockaddr_in client;
int len = 0;
while(1)
{
int sock = accept(listenSock,(struct sockaddr*)&client,&len);
if(sock < 0)
{
perror("accept");
return 5;
}
printf("get a client!ip is %s,port is %d\n",inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
pthread_t tid;
int ret = pthread_create(&tid,NULL,thread_hander,&sock);
if(ret < 0)
{
perror("pthread_create");
return 6;
}
pthread_detach(tid);
}
return 0;
}
//client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main(int argc, const char* argv[])
{
if(argc != 3)
{
printf("input error\n");
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sock,(struct sockaddr*)&server,sizeof(server));
if(connect < 0)
{
perror("connect");
return 3;
}
char buf[1024];
while(1)
{
printf("send#");
fflush(stdout);
ssize_t _s = read(0,buf,sizeof(buf)-1);
if(_s > 0)
{
buf[_s - 1] = 0;
if(write(sock,buf,sizeof(buf)-1) < 0)
{
break;
}
ssize_t s = read(sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("server echo#%s\n",buf);
}
}
else
{
perror("read");
return 4;
}
}
return 0;
}