(3)基于linux的socket编程TCP半双工client-server聊天程序

所谓半双工通信,即通信的双方都可以实现接发数据,但是有一个限制:只能一方发一方收,之后交换收发对象。也就是所谓的阻塞式通信方式。

一、基本框架:

1、首先搞清楚我们进行编程所处的位置:

这里写图片描述

TCP编程,具有可靠的传输的特性,而实现可靠传输的功能并非我们将要做的事,我们要做的就是在内核实现的基础上调用API接口直接使用。所以我们所处的位置就是位于应用层面与系统层面之间的。我们觉得弄清楚这点是实现整个通信程序的重中之重。

2、弄清楚此次的目的:实现伪半双工通信

    为什么是伪半双工?应为真正的半双工是通信双方都可以随时揭发数据(只是限制不能同时发,同时收,在同一时刻只能由乙方发,乙方收),而我们要实现的是”傻瓜式“的你一句我一句,因为不是全双工,而类似于半双工,我也不知道有没有更加准确的说法,就暂且叫他伪半双工吧!

 

3、TCP编程框架:

这里写图片描述

二、所用到的结构体与函数:

1、几个结构体:

(1)ipv4套接字地址结构体:

1
2
3
4
5
6
7
struct sockaddr_in{
     uint8_t             sin_len;
     sa_famliy_t         sin_fanliy; /*协议家族*/
     in_port_t           sin_port; /*端口号*/
     struct in_addr      sin_addr; /*IP地址,struct in_addr{in_addr_t s_addr;}*/
     char                sin_zero[ 8 ];
};

2、建基本框架所使用的函数,这些函数都是系统调用,失败会返回返回一个errno错误标识:

#include<sys/socket.h>

(1)socket:

int socket(int domain,int type, int protocol);/* 创建一个套接字: 返回值:    创建成功返回一个文件描述符(0,1,2已被stdin、stdout、stderr占用,所以从3开始)    失败返回-1。 参数:    domain为协议家族,TCP属于AF_INET(IPV4);    type为协议类型,TCP属于SOCK_STREAM(流式套接字);    最后一个参数为具体的协议(IPPOOTO_TCP为TCP协议,前两个已经能确定该参数是TCP,所以也可以填0) */

(2)bind()

int bind(int sockfd,const struct sockaddr * addr,socklen_t addrlen);/* 将创建的套接字与地址端口等绑定 返回值:成功返回0,失败返回-1. 参数:    sockfd为socket函数返回接受的文件描述符,    addr为新建的IPV4套接字结构体    注意:定义若是使用struct sockaddr_in(IPV4结构体)定义,但是该参数需要将struct sockaddr_in *类型地址强转为struct sockaddr *类型(struct sockaddr是通用类型)。    最后一个参数为该结构体所占字节数。 */

(3)listen()

int listen(int sockfd,int backlog);/* 对创建的套接字进行监听,监听有无客户请求连接 返回值:有客户请求连接时,返回从已完成连接的队列中第一个连接(即完成了TCP三次握手的的所有连接组成的队列),否则处于阻塞状态(blocking)。 参数: sockfd依然为socket函数返回的文件描述符; blocklog为设定的监听队列的长度。可设为5、10等值但是不能大于SOMAXCONN(监听队列最大长度) */

这里写图片描述

监听对立包括请求建立过程中的两个子队列:未完成连接的对列和已完成连接的对立。区分的标识就是:是否完成TCP三次握手的过程。服务器从已完成连接的队列中按照先进先出(FIFO)的原则进行接收。

(4)connect()和accept()

int connect(int sockfd,const struct sockaddr * addr,socklen_t addrlen);/* 客户端请求连接 返回值:成功返回0,失败返回-1 参数:客户端的socket文件描述符,客户端的socket结构体地址以及结构体变量长度 */int accept(int sockfd,struct sockaddr * addr,socklen_t * addrlen);/* 从监听队列中接收已完成连接的第一个连接 返回值:成功返回0,失败返回-1 参数:服务器socket未见描述符,服务器的socket结构体地址以及结构体变量长度 */

(5)send() 和 recv()

ssize_t send(int sockfd,const void * buf,size_t len,int flags);/* 发送数据 返回值:成功返回发送的字符数,失败返回-1 参数:buf为写缓冲区(send_buf),len为发送缓冲区的大小,flags为一个标志,如MSG_OOB表示有紧急带外数据等 */ssize_t recv(int sockfd,void *buf, size_t len, int flags);/* 接收数据 返回值参数与send函数相似 不过send是将buf中的数据向外发送,而recv是将接收到的数据写到buf缓冲区中。 */

(6)close()

int close(int fd);/* 关闭套接字,类似于fclose,fd为要关闭的套接字文件描述符 失败返回-1,成功返回0 */

3、其它函数:

(1)字节序转换函数:

/*由于我们一般普遍所用的机器(x86)都是小端存储模式或者说叫做小端字节序,而网络传输中采用的是大端字节序,所以要进行网络通讯,就必须将进行字节序的转换,之后才可以进行正常信息传递。*/uint32_t htonl(uint32_t hostlong);/*主机字节序转换成网络字节序*/uint32_t ntohl(uint32_t netlong);/*网络字节序转换成主机字节序*/

(2)地址转换函数

/*类似的原因,由于网络传输是二进制比特流传输,所以必须将我们的常用的点分十进制的IP地址,与网络字节序的IP源码(二进制形式)进行互相转换才可以将数据传送到准确的地址*/int inet_aton(const char * cp,struct in_addr * inp);/*将字符串cp表示的点分十进制转换成网络字节序的二进制形式后存储到inp中*/char * inet_ntoa(struct in_addr * in);/*将网络字节序的二进制形式转换成点分十进制的字符串形式,返回该字符串的首地址*/in_addr_t inet_addr(const char * cp);/*与inet_aton的功能相同*/

三、代码实现:

(1)服务器:服务器由于不知道客户何时建立连接,所以必须绑定端口之后进行监听(socket,bind,listen)

(2)客户端:客户端只需要向服务器发起请求连(connect),而不需要绑定于监听的步骤。

 

1、服务器代码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
include <sys/socket.h>
include <netinet/ in .h>
include <arpa/inet.h>
include <signal.h>
include <assert.h>
include <stdio.h>
include <unistd.h>
include <string.h>
include <stdlib.h>
include <errno.h>
 
# define BUF_SIZE  1024 //缓冲区大小宏定义
 
int  main ( int  argc,char * argv[]) /*接收IP地址和端口号*/
{
     const  char * ip = argv[ 1 ];
     int  port = atoi(argv[ 2 ]); /*将输入的端口号由字符串转换为整数类型*/
     /*结构体定义与初始化*/
     struct sockaddr_in address;
     bzero(&address,sizeof(address)); /*初始化清零,类似于memset函数*/
     address.sin_family = AF_INET;
     inet_pton(AF_INET,ip,&address.sin_addr); /*inet_pton是inet_aton的升级版,随IPV6的出现而出现*/
     address.sin_port = htons(port); /*将小端字节序转换为网络字节序*/
 
     int  sock = socket(PF_INET, SOCK_STREAM,  0 ); /*创建套接字*/
     assert(sock >=  0 );
 
     int  ret = bind(sock,(struct sockaddr*)&address,sizeof(address)); /*绑定IP地址、端口号等信息*/
     assert(ret != - 1 );
     ret = listen(sock, 5 ); /*监听有无连接请求*/
     assert(ret != - 1 );
 
     struct sockaddr_in client;
     socklen_t client_addrlength = sizeof(client);
     int  connfd = accept(sock,(struct sockaddr *)&client,&client_addrlength); /*从监听队列中取出第一个已完成的连接*/
     char buffer_recv[BUF_SIZE]={ 0 };
     char buffer_send[BUF_SIZE]={ 0 };
     while ( 1 ){
         if (connfd <  0 ){
             printf( "errno is : %d\n" ,errno);
         }
         else {
             memset(buffer_recv, 0 ,BUF_SIZE);
             memset(buffer_send, 0 ,BUF_SIZE); /*每次需要为缓冲区清空*/
 
             ret = recv(connfd, buffer_recv, BUF_SIZE- 1 0 );
             if (strcmp(buffer_recv, "quit\n" ) ==  0 ){
                 printf( "Communications is over!\n" );
                 break ;
             } /*recv为quit表示客户端请求断开连接,退出循环*/
             printf( "client:%s" , buffer_recv);
 
             printf( "server:" );
             fgets(buffer_send,BUF_SIZE,stdin);
             send(connfd,buffer_send,strlen(buffer_send), 0 );
             if (strcmp(buffer_send, "quit\n" ) ==  0 ){
                 printf( "Communications is over!\n" );
                 break ;
             } /*send为quit表示服务器请求断开连接,退出循环*/
         }
     }
     close(connfd);
     close(sock);
     return  0 ;
}

assert函数详解:https://www.cnblogs.com/ggzss/archive/2011/08/18/2145017.html

2、客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
include <sys/socket.h>
include <netinet/ in .h>
include <arpa/inet.h>
include <signal.h>
include <assert.h>
include <stdio.h>
include <unistd.h>
include <string.h>
include <stdlib.h>
 
#define BUF_SIZE  1024
 
int  main ( int  argc,char * argv[])
{
     const  char * ip = argv[ 1 ];
     int  port = atoi(argv[ 2 ]);
 
     struct sockaddr_in server_address;
     bzero(&server_address,sizeof(server_address));
     server_address.sin_family = AF_INET;
 
     inet_pton(AF_INET,ip,&server_address.sin_addr);
     server_address.sin_port = htons(port);
 
     int  sockfd = socket(PF_INET, SOCK_STREAM,  0 );
     assert(sockfd >=  0 );
 
     int  connfd = connect(sockfd, (struct sockaddr *)&server_address,sizeof(server_address));   
 
     char buffer_recv[BUF_SIZE] = { 0 };
     char buffer_send[BUF_SIZE] = { 0 };
     while ( 1 ){
         if (connfd <  0 ){
             printf( "connection failed\n" );
         }
         else {
             memset(buffer_send, 0 ,BUF_SIZE);
             memset(buffer_recv, 0 ,BUF_SIZE);
 
             printf( "client:" );
             fgets(buffer_send,BUF_SIZE,stdin);
             send(sockfd, buffer_send, strlen(buffer_send),  0 );
             if (strcmp(buffer_send, "quit\n" ) ==  0 ){
                 printf( "Communications is over!\n" );
                 break ;
             } /*send为quit表示客户端请求断开连接,退出循环*/
 
             int  ret = recv(sockfd,buffer_recv,BUF_SIZE- 1 , 0 );
             if (strcmp(buffer_recv, "quit\n" ) ==  0 ){
                 printf( "Communications is over!\n" );
                 break ;
             } /*recv为quit表示服务器请求断开连接,退出循环*/
             printf( "server:%s" ,buffer_recv);
 
         }
     }  
     close(connfd);
     close(sockfd);
 
     return  0 ;
}

1、 发起连接:如果说服务器通过listen吊用来被动的接受链接,那么客户端需要通过如下系统调用来主动的与服务器简历链接:

#include <sys/types.h>

#include <sys/socket.h>

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

sockfd 参数由socket调用返回一个socket。serv_addr参数是服务器监听的soccket地址,addrlen参数则指定这个地址的长度。

connect成功时,返回0,一旦成功的建立链接,sockfd就唯一的表示了这个链接,客户端就可以通过读写sockfd来与服务器进行通信。

2、关闭连接:关闭一个链接实际上就是关闭该链接对应的socket,这可以通过关闭文件描述符的系统调用来完成。

#include<unistd.h>

int close(int fd)

fd参数是待关闭的socket,不过close系统调用并非立即关闭一个链接,而是将fd的引用参数减一。只有当fd的引用参数基数为0的时候,才真正关闭连接。多进程程序中,一次fork系统调用默认将使得父进程中打开的soket的引用计数加一,因此我们必须在父进程和子进程中都对该socket执行clise调用才能将连接关闭。

  如果无论如何都要立即终止链接,可以使用如下的shutdown调用。(专门为网络编程设计的)

#include<sys/socket.h>

int shutdown(itn sockfd,int howto);

sockfd参数是待关闭的socket,howto参数决定了shutdown的行为,他可以取如下的值:

SHUT_RD :关闭sockfd上读的这一半,应用程序不能在针对socket文件描述符执行读操作,并且该socket接收缓冲区中的数据被丢弃。

SHUT_WR:关闭sockfd上的写的这一半。sockfd的发送缓冲区中的数据会在真正关闭之前都发送出去,应用程序不再对该socket执行写操作。连接处于半关闭状态。

SHUT_RDWR:同时关闭sockfd上的读和写。

3、读和写:

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

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

 

recv读取sockfd上的数据,buf和len分别制定缓冲区的位置和大小,recv成功时候返回实际读取到的数据的长度,他可能小于我们期望的长度len,因此我们可以多次调用recv,才能读取到完整的数据。recv可能返回0,这意味着通信双方已经关闭连接了。recv出错时返回-1,并设置errno。

send往sockfd中写数据,buf和len分别制定缓冲区的位置和大小。send时候,成功返回实际写入的数据的长度,失败返回-1,并设置error。

 

 

 

 

 

 

 

 

转载于:https://www.cnblogs.com/yjds/p/8597367.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值