socket通讯基础

各位兄弟,在学习Linux编程基础以前,必定要先学习Linux基础知识和计算机网络基础知识,若是对这两方面的基础知识和基本概念不熟,谈不上Linux编程和网络通讯编程。程序员

1、socket通讯的概念

socket也称做“套接字”,描述了计算机的IP地址和端口,运行在计算机中的程序之间采用socket进行数据通讯。通讯的两端都有socket,它是一个通道,数据在两个socket之间进行传输。编程

socket把复杂的TCP/IP协议族隐藏在socket接口后面,对程序员来讲,只要用好socket相关的函数,就能够完成数据通讯。数组

2、套接字(socket)

TCP提供了流(stream)和数据报(datagram)两种通讯机制,因此套接字也分为流套接字和数据报套接字。服务器

流套接字的类型是SOCK_STREAM,它提供的是一个有序、可靠、双向字节流的链接,所以发送的数据能够确保不会丢失、重复或乱序到达,并且它还有出错后从新发送的机制(就像两我的在打电话,聊天您一句我一句,有来有往,没听清楚就再说一次)。网络

数据报套接的类型是SOCK_DGRAM,它不须要创建和维持一个链接,采用UDP/IP协议实现。它对能够发送的数据的长度有限制,数据报做为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,可是它的速度比较高,由于它不须要创建和维持链接(就像一我的向另外一我的发短信,一条短信发出去,对方不必定能收到)。数据结构

在实际开发中,数据报套接字(即UDP)的应用场景极少,本章节只介绍流套接字。dom

3、socket通讯的过程

1)服务端程序将一个套接字绑定到指定的ip地址和端口,并经过此套接字等待和监听客户的链接请求。socket

2)客户程序向服务端程序绑定的地址和端口发出链接请求。函数

3)服务端接受链接请求。学习

4)客户端和服务端经过读写套接字进行通讯。

在这里插入图片描述

4、客户/服务端模式

在TCP/IP网络应用中,两个程序之间通讯模式是客户/服务端模式(client/server),客户/服务端也叫做客户/服务器,各人习惯。

服务端的工做流程

1)建立服务端的socket。

2)把服务端用于通讯的地址和端口绑定到socket上。

3)把socket设置为监听模式。

4)接受客户端的链接。

5)与客户端通讯,接收客户端发过来的报文后,回复处理结果。

6)不断的重复第5)步,直到客户端断开链接。

7)关闭socket,释放资源。

服务端示例(book242.cpp)

/*
 * 程序名:book242.cpp,此程序用于演示socket通讯的服务端
 * 做者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_public.h"

int main()
{
  // 第1步:建立服务端的socket。
  int listenfd = socket(AF_INET,SOCK_STREAM,0);  

  // 第2步:把服务端用于通讯的地址和端口绑定到socket上。
  struct sockaddr_in servaddr;    // 服务端地址信息的数据结构。
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;  // 协议族,在socket编程中只能是AF_INET。
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);          // 任意ip地址。
  //servaddr.sin_addr.s_addr = inet_addr("118.89.50.198"); // 指定ip地址。
  servaddr.sin_port = htons(5051);  // 指定通讯端口。
  if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
  { perror("bind"); close(listenfd); return -1; }

  // 第3步:把socket设置为监听模式。
  if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }

  // 第4步:接受客户端的链接。
  int  clientfd;                  // 客户端的socket。
  int  socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
  struct sockaddr_in clientaddr;  // 客户端的地址信息。
  clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
  printf("客户端(%s)已链接。\n",inet_ntoa(clientaddr.sin_addr));

  // 第5步:与客户端通讯,接收客户端发过来的报文后,回复ok。
  char buffer[1024];
  while (1)
  {
    memset(buffer,0,sizeof(buffer));
    if (recv(clientfd,buffer,sizeof(buffer),0)<=0) break;   // 接收客户端的请求报文。
    printf("接收:%s\n",buffer);

    strcpy(buffer,"ok");
    if (send(clientfd,buffer,strlen(buffer),0)<=0) break;   // 向客户端发送响应结果。
    printf("发送:%s\n",buffer);
  }

  // 第6步:关闭socket,释放资源。
  close(listenfd); close(clientfd);  
}

二、客户端的工做流程

1)建立客户端的socket。

2)向服务器发起链接请求。

3)与服务端通讯,发送一个报文后等待回复,而后再发下一个报文。

4)不断的重复第3)步,直到所有的数据被发送完。

5)第4步:关闭socket,释放资源。

客户端示例(book241.cpp)

/*
 * 程序名:book241.cpp,此程序用于演示socket的客户端
 * 做者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include "_public.h"

int main()
{
  // 第1步:建立客户端的socket。
  int sockfd = socket(AF_INET,SOCK_STREAM,0); 

  // 第2步:向服务器发起链接请求。
  struct hostent* h; 
  if ( (h = gethostbyname("118.89.50.198")) == 0 )   // 指定服务端的ip地址。
  { perror("gethostbyname"); close(sockfd); return -1; }
  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(5051); // 指定服务端的通讯端口。
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
  if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)  // 向服务端发起链接清求。
  { perror("connect"); close(sockfd); return -1; }

  char buffer[1024];
  
  // 第3步:与服务端通讯,发送一个报文后等待回复,而后再发下一个报文。
  for (int ii=0;ii<3;ii++)
  {
    memset(buffer,0,sizeof(buffer));
    sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
    if (send(sockfd,buffer,strlen(buffer),0)<=0) break;    // 向服务端发送请求报文。
    printf("发送:%s\n",buffer);
    
    memset(buffer,0,sizeof(buffer));
    if (recv(sockfd,buffer,sizeof(buffer),0)<=0) break;    // 接收服务端返回的结果。
    printf("接收:%s\n",buffer);
  }

  // 第4步:关闭socket,释放资源。
  close(sockfd);
}

在运行程序以前,必须保证服务器的防火墙已经开通了网络访问策略,若是您不明这句话的意思,说明您的Linux基础知识不够,请先学习Linux基础知识以后再来学习socket通讯。

先启动服务端程序book242,服务端启动后,进入等待客户端链接状态,而后启动客户端。

客户端的输出以下:

在这里插入图片描述
服务端的输出以下:

在这里插入图片描述

5、注意事项

一、别去纠缠细节

在socket通讯的客户端和服务器的程序里,出现了多种数据结构,调用了多个函数,涉及到不少方面的知识,对初学者来讲,更重要的是了解socket通讯的过程、每段代码的用途和函数调用的功能,不要去纠缠这些结构体和函数的参数,这些函数和参数虽然比较多,但能够修改的很是少,别抄错就能够了,须要注意的地方我会提出。

二、服务端程序绑定地址

若是服务器有多个网卡,多个IP地址,socket通讯能够指定用其中一个地址来进行通讯,也能够任意ip地址。

1)指定ip地址的代码

m_servaddr.sin_addr.s_addr = inet_addr("192.168.149.129"); // 指定ip地址

2)任意ip地址的代码

m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本主机的任意ip地址

在实际开发中,采用任意ip地址的方式比较多。

三、服务端程序绑定的通讯端口

m_servaddr.sin_port = htons(5000);  // 通讯端口

四、客户端程序指定服务端的ip地址

struct hostent* h; // ip地址信息的数据结构
if ( (h = gethostbyname("192.168.149.129")) == 0 )
{ perror("gethostbyname"); close(sockfd); return -1; }

五、客户端程序指定服务端的通讯端口

servaddr.sin_port = htons(5000);

六、send函数

send函数用于把数据经过socket发送给对端。不管是客户端仍是服务端,应用程序都用send函数来向TCP链接的另外一端发送数据。

函数声明:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

sockfd为已创建好链接的socket。

buf为须要发送的数据的内存地址,能够是C语言基本数据类型变量的地址,也能够数组、结构体、字符串,内存中有什么就发送什么。

len须要发送的数据的长度,为buf中有效数据的长度。

flags填0, 其余数值意义不大。

函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。

注意,就算是网络断开,或socket已被对端关闭,send函数不会当即报错,要过几秒才会报错。

若是send函数返回的错误(<=0),表示通讯链路已不可用。

七、recv函数

recv函数用于接收对端socket发送过来的数据。

recv函数用于接收对端经过socket发送过来的数据。不管是客户端仍是服务端,应用程序都用recv函数接收来自TCP链接的另外一端发送过来数据。

函数声明:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

sockfd为已创建好链接的socket。

buf为用于接收数据的内存地址,能够是C语言基本数据类型变量的地址,也能够数组、结构体、字符串,只要是一块内存就好了。

len须要接收数据的长度,不能超过buf的大小,不然内存溢出。

flags填0, 其余数值意义不大。

若是socket的对端没有发送数据,recv函数就会等待,若是对端发送了数据,函数返回接收到的字符数。出错时返回-1,错误信息errno被标记。若是socket被对端关闭,返回值为0。

若是recv函数返回的错误(<=0),表示通讯通道已不可用。

八、服务端有两个socket

对服务端来讲,有两个socket,一个是用于监听的socket,还有一个就是客户端链接成功后,由accept函数建立的用于与客户端收发报文的socket。

九、程序退出时先关闭socket

socket是系统资源,操做系统打开的socket数量是有限的,在程序退出以前必须关闭已打开的socket,就像关闭文件指针同样,就像delete已分配的内存同样,极其重要。

值得注意的是,关闭socket的代码不能只在main函数的最后,那是程序运行的理想状态,还应该在main函数的每一个return以前关闭。

6、相关的库函数

一、socket函数

socket函数用于建立一个新的socket,也就是向系统申请一个socket资源。socket函数用户客户端和服务端。

函数声明:

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

参数说明:

domain:协议域,又称协议族(family)。经常使用的协议族有AF_INET、AF_INET六、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(SOCK_STREAM)是一种面向链接的socket,针对于面向链接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无链接的socket,对应于无链接的UDP服务应用。

protocol:指定协议。经常使用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

说了一大堆废话,第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM,第三个参数只能填0。

除非系统资料耗尽,socket函数通常不会返回失败。

二、gethostbyname函数

把ip地址或域名转换为hostent 结构体表达的地址。

函数声明:

struct hostent *gethostbyname(const char *name);

参数name,域名或者主机名,例如"192.168.1.3"、"www.freecplus.net"等。

返回值:若是成功,返回一个hostent结构指针,失败返回NULL。

gethostbyname只用于客户端。

gethostbyname只是把字符串的ip地址转换为结构体的ip地址,只要地址格式没错,通常不会返回错误。函数失败不会设置errno的值。

三、connect函数

向服务器发起链接请求。

函数声明:

int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);

函数说明:connect函数用于将参数sockfd 的socket 连至参数serv_addr
指定的服务端,参数addrlen为sockaddr的结构长度。

返回值:成功则返回0, 失败返回-1, 错误缘由存于errno 中。

connect函数只用于客户端。

若是服务端的地址错了,或端口错了,或服务端没有启动,connect必定会失败。

四、bind函数

服务端把用于通讯的地址和端口绑定到socket上。

函数声明:

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

参数sockfd,须要绑定的socket。

参数addr,存放了服务端用于通讯的地址和端口。

参数addrlen表示addr结构体的大小。

若是绑定的地址错误,或端口已被占用,bind函数必定会报错,不然通常不会返回错误。

五、listen函数

listen函数把主动链接套接字变为被动链接的套接字,使得这个socket能够接受其它socket的链接请求,从而成为一个服务端的socket。

函数声明:

int listen(int sockfd, int backlog);

返回:0-成功, -1-失败

参数sockfd是已经被bind过的套接字。socket函数返回的套接字是一个主动链接的套接字,在服务端的编程中,程序员但愿这个套接字能够接受外来的链接请求,也就是被动等待客户端来链接。因为系统默认时认为一个套接字是主动链接的,因此须要经过某种方式来告诉系统,程序员经过调用listen函数来完成这件事。

参数backlog,这个参数涉及到一些网络的细节,比较麻烦,填五、10都行,通常不超过30。

当调用listen以后,服务端的套接字就能够调用accept来接受客户端的链接请求。

listen函数通常不会返回错误。

六、accept函数

服务端接受客户端的链接。

函数声明:

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

参数sockfd是已经被listen过的套接字。

参数addr用于存放客户端的地址信息,用sockaddr结构体表达,若是不须要客户端的地址,能够填0。

参数addrlen用于存放addr参数的长度,若是addr为0,addrlen也填0。

accept函数等待客户端的链接,若是没有客户端连上来,它就一直等待,这种方式称之为阻塞。

accept等待到客户端的链接后,建立一个新的套接字,函数返回值就是这个新的套接字,服务端使用这个新的套接字和客户端进行报文的收发。

accept在等待的过程当中,若是被中断或其它的缘由,函数返回-1,表示失败,若是失败,能够从新accept。

七、函数小结

服务端函数调用的流程是:socket->bind->listen->accept->recv/send->close

客户端函数调用的流程是:socket->connect->send/recv->close

其中send/recv能够进行屡次交互。

7、课后做业

1)把book241.cpp和book242.cpp抄下来,编译运行,试试修改参数再运行。

2)book241.cpp和book242.cpp程序中,有些代码不能动,有些代码能够动,把能动的都动一下,就算是抄代码,也要抄个明白。

3)服务端的accept函数会阻塞,阻塞是专业名词,即等待,能够用代码测试一下。

4)不论是服务端仍是客户端recv函数也会阻塞,能够用代码测试一下。

5)修改book241.cpp和book242.cpp,实现点对点的聊天功能,用户在客户端输入一个字符串,而后发送给服务端,服务端收到客户端的报文后,也提示用户输入一个字符串,返回给客户端,若是服务端收到客户端的报文是“bye”通讯结束。

6)若是以上做业都能完成,建议再把本文章的内容再看一次,对文章开始部分的理论知识将有新的理解。

8、版权声明

C语言技术网原创文章,转载请说明文章的来源、做者和原文的连接。
来源:C语言技术网(www.freecplus.net)
做者:码农有道

若是文章有错别字,或者内容有错误,或其余的建议和意见,请您留言指正,很是感谢!!!

相关文章
相关标签/搜索