基于TCP协议的C/S端程序的基本流程

基于TCP协议的C/S端程序的基本流程

  • 服务端通讯流程
  • TCP服务端基本通讯示例
  • 客户端通讯流程
  • TCP客户端基本通讯示例
  • 总结:图解简单的TCP通讯过程
  • 总结:通讯过程中用到的函数参数及返回值
      • socket()函数
      • 字节序转换函数
      • listen()函数
      • accept()和connect()函数
      • send()和recv()函数
  • 注意事项

服务端通讯流程

服务端:
	1、通过socket()函数创建用于接收连接请求的socket
	2、构造主机连接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
	3、绑定sockaddr_in结构体和socket
	4、通过listen()函数将stocket设置为监听模式
	5、调用accept()函数等待接收客户端的连接请求,并返回用于传递信息的stocket
	6、和客户端进行通信
	7、调用close()关闭socket

TCP服务端基本通讯示例

// 简单的TCP回送服务 服务端示例
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main(int argc,char *argv[])
{
  if (argc!=2)
  {
    printf("Using:./server port\nExample:./server 5005\n\n"); return -1;
  }
 
  // 第1步:通过socket()函数创建用于接收连接请求的socket
  int listenfd;
  if ( (listenfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
 
  // 第2步:构造主机连接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
  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("111.111.111.111"); // 指定ip地址。
  servaddr.sin_port = htons(atoi(argv[1]));  // 指定通信端口。
	
	// 第3步: 绑定sockaddr_in结构体和socket
  if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
  { perror("bind"); close(listenfd); return -1; }
 
  // 第4步:通过listen()函数将stocket设置为监听模式
  if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
 
  // 第5步:调用accept()函数等待接收客户端的连接请求,并返回用于传递信息的stocket
  // 这里也可以调用高级IO函数 或 使用多进程/线程的模式 实现与多个客户端同时通讯
  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));
 
  // 第6步:与客户端通信,接收客户端发过来的报文后,回复ok。
  char buffer[1024];//创建缓冲区
  while (1)
  {
    int iret;
    memset(buffer,0,sizeof(buffer));
    if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。
    {
       printf("iret=%d\n",iret); break;  
    }
    printf("接收:%s\n",buffer);
 
    strcpy(buffer,"ok");
    if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。
    { perror("send"); break; }
    printf("发送:%s\n",buffer);
  }
 
  // 第7步:调用close()关闭socket
  close(listenfd); 
  close(clientfd);
}

这里需要注意,如果第五步中同时创建了多个用于和客户端通讯的socket,那么在和每个客户端通讯结束的时候,需要注意释放socket。

客户端通讯流程

客户端:
	1、通过socket()函数创建用于请求和通讯的socket
	2、构造主机链接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
	3、调用connect()函数向服务端发送连接请求
	4、与服务端进行通讯
	5、调用close()关闭socket

TCP客户端基本通讯示例

// 简单的回送服务 客户端示例
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main(int argc,char *argv[])
{
  if (argc!=3)
  {
    printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n"); return -1;
  }
 
  // 第1步:通过socket()函数创建用于请求和通讯的socket
  int sockfd;
  if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
 
  // 第2步:构造主机链接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
  struct hostent* h;
  if ( (h = gethostbyname(argv[1])) == 0 )   // 指定服务端的ip地址。
  { printf("gethostbyname failed.\n"); close(sockfd); return -1; }
  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
	
	// 第3步:调用connect()函数向服务端发送连接请求
  if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)  // 向服务端发起连接清求。
  { perror("connect"); close(sockfd); return -1; }
 
  char buffer[1024];
 
  // 第4步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
  for (int ii=0;ii<3;ii++)
  {
    int iret;
    memset(buffer,0,sizeof(buffer));
    sprintf(buffer,"这是第%d个消息,编号%03d。",ii+1,ii+1);
    if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。
    { perror("send"); break; }
    printf("发送:%s\n",buffer);
 
    memset(buffer,0,sizeof(buffer));
    if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。
    {
       printf("iret=%d\n",iret); break;
    }
    printf("接收:%s\n",buffer);
  }
 
  // 第5步:调用close()关闭socket
  close(sockfd);
}

总结:图解简单的TCP通讯过程

基于TCP协议的C/S端程序的基本流程_第1张图片

总结:通讯过程中用到的函数参数及返回值

socket()函数

#include 

int socket(int domain, int type, int protocol);
// 创建成功返回文件描述符,创建失败返回-1

domain : 套接字中使用的协议族
    PF_INET : IPv4互联网协议族
    PF_INET6 : IPv6互联网协议族
    PF_LOCAL : 本地通信的UNIX协议族
    PF_PACKET : 底层套接字的协议族
    PF_IPX : IPX Novell协议族

type : 套接字数据传输类型信息,套接字的数据传输方式。
    SOCK_STREAM : 面向连接的套接字,TCP
                可靠的,按序传递的,基于字节的面向连接的数据传输方式的套接字。
    SOCK_DGRAM : UDP
                不可靠的,不按序传递的,以数据的告诉传输为目的的套接字。

protocol : 计算机间通信使用的协议信息
    前两个参数基本确定了协议类型,第三个参数一般传0.
    IPPROTO_TCP : TCP
    IPPROTO_UDP : UDP

注意:默认情况下只能打开1024(0-1023)个文件描述符,即可以创建1020个套接字,0、1、2三个描述符默认表示标准输入、输出和错误

字节序转换函数

#include 

//主机字节序转网络字节序
unsigned short htons(unsigned short);
unsigned long htonl(unsigned long);

//网络字节序转主机字节序
unsigned short ntohs(unsigned short);
unsigned long ntohl(unsigned long);

// 完成字符串转换及网络字节序转换
in_addr_t inet_addr(const char *string);

// 完成字符串转换及网络字节序转换,成功返回1,失败返回0
int inet_aton(const char * string, struct in_addr *addr);

// 32位整型ip地址转换为字符串
char *inet_ntoa(struct in_addr adr);

字节顺序:字节顺序是指占内存大余一个字节类型的数据再内存中存放的顺序。(小端/大端)
基于TCP协议的C/S端程序的基本流程_第2张图片
由于不同的主机之中,因为CPU的不同,所以可能会使用不同的字节序去存储数据。如果客户端和服务端的字节序不同,那么解析出的数据也就不同,所以就需要规定一个统一的字节序进行数据的传输,这个统一的字节序就称为网络字节序,一般采用大端字节序

listen()函数

#include  /* See NOTES */
#include 
//listen()将sockfd设为监听状态,并且最多允许有backlog个客户端处于连接待状态,
//如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

int listen(int sockfd, int backlog);
    sockfd:socket文件描述符
    backlog:在Linux 系统中,它是指已经建立好链接,等待accetp()的请求队列的长度

accept()和connect()函数

#include
//用于客户端
//TCP客户用connect函数来发起与TCP服务器的连接请求
//返回:若成功则为0,若出错则为-1

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
    sockfd:socket函数返回的套接字描述符,
    servaddr:只想套接字地址结构体的指针
    addrlen:地址结构的大小。
//备注:套接字地址结构必须含有服务器的IP地址和端口号。
#include 
#include 
//用于服务端
//提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符,出错时返回-1
//注:客户端和服务端进行通讯所使用等socket是accept()函数返回的socket
//之前所使用等socket只能用来建立客户-服务器之间的TCP链接

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
    sockfd:利用系统调用socket()建立的套接字描述符
    addr:指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写。
    addrlen:一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;一般设置为sizeof(struct sockaddr_in)    

如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接请求出现。
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
一般来说,实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;若有,把数据拷贝出来,删掉接收到的数据包,创建新的socket与客户发来的地址建立连接,若没有,就阻塞等待;

send()和recv()函数

#include   
#include 
//用来将数据由指定的socket 传给对方主机
//返回值:成功则返回实际传送出去的字符数, 失败返回-1. 错误原因存于errno

ssize_t send(int socket, const void * buf, int len, unsigned int falgs);
    socket:为已建立好连接的socket. 
    buf:想要发送数据的指针
    len:需要发送的数据长度,不一定和buf的大小一样
    flags:一般设0, 其他数值定义如下:
	 		MSG_OOB 传送的数据以out-of-band 送出.
	 	 	MSG_DONTROUTE 取消路由表查询
	  	    MSG_DONTWAIT 设置为不可阻断运作
	        MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断.

#include   
#include 
//recv函数用于接收对端socket发送过来的数据。
//函数返回已接收的字符数。出错时返回-1,失败时不会设置errno的值。
//如果socket的对端没有发送数据,recv函数就会阻塞

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    sockfd:为已建立好连接的socket。
    buf:为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。
    len:需要接收数据的长度,不能超过buf的大小,否则内存溢出。
    flags:填0, 其他数值意义不大。

注意事项

由于TCP使用的是面向数据流的形式发送数据,所以如果短时间内同时发送多次数据,那么有可能会把这多次的数据拼接成一条数据进行发送,这个问题被称为粘包。例如,发送方发送两个字符串 “hello” 和 “world”,接收方却一次性收到了“helloworeld”。
同时相对的,如果发送的数据过长,也有可能会分为多次进行发送,这个问题称为分包,例如,发送放发送字符串“helloworld",接收方却收到两个字符串“hello” 和 “world”。
但是,TCP协议会保证
1、接收方接收到的顺序不变
2、被分包的数据在发送的过程中,之间不会插入其他数据

你可能感兴趣的:(计算机网络,tcp/ip,c语言,网络)