基于Linux的SOCKET編程之TCP半雙工Client-Server聊天程序

轉自:http://blog.csdn.net/apollon_krj/article/details/53398448#0-tsina-1-64987-397232819ff9a47a7b7e80a40613cfe1

所謂半雙工通信,即通信雙方都可以實現接發數據,但是有一個限制:只能一方發一方收,之后交換收發對象。也就是所謂的阻塞式的通訊方式。

一、基本框架:

1、首先搞清我們進行編程所處的的位置:

這里寫圖片描述
TCP編程,具有可靠傳輸的特性,而實現可靠傳輸的功能并非我們將要做的事(這些事),我們要做的就是在內核實現的基礎上調用系統的API接口直接使用。所以我們所處的位置就是位于應用層面與系統層面之間的。我覺得弄清這點是實現整個通信程序的重中之重。

這里寫圖片描述

2、弄清楚此次的目的:實現偽半雙工的通信

為什么說是“偽”半雙工通信,因為真正的半雙工是通信雙方都可以隨時接發數據(只是限制不能同時發,也不能同時收,在同一時刻只能由一方發送,一方接收),而我們要實現的是“傻瓜式”的你一句我一句,因為不是全雙工,而類似于半雙工,我也不知道有沒有更準確的說法,就暫且叫它“偽半雙工吧”!

3、TCP編程框架:

下面這張圖是很多博客中都使用到的一張流圖,其原圖都來自于UNIX網絡編程卷1:套接字聯網API 【史蒂文斯 (W.Richard Stevens)、芬納 (Bill Fenner) 、 魯道夫 (Andrew M.Rudoff)著】這本書。核心思想都是一樣的,所以就直接貼上了:?
這里寫圖片描述

二、所用到的結構體與函數:

1、幾個結構體:

(1)、IPV4套接字地址結構體:

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];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(2)、通用套接字地址結構體:

struct sockaddr{uint8_t       sa_len;sa_famliy     sa_famliy;char          sa_data[14];
};
  • 1
  • 2
  • 3
  • 4
  • 5

2、建基本框架所使用的函數,這些函數都是系統調用(man 2 function),失敗都會設置一個errno錯誤標志:

#include<sys/socket.h>
  • 1

(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)
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(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是通用類型)。最后一個參數為該結構體所占字節數。
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(3)、listen:

int listen(int sockfd,int backlog);
/*
對創建的套接字進行監聽,監聽有無客戶請求連接
返回值:有客戶請求連接時,返回從已完成連接的隊列中第一個連接(即完成了TCP三次握手的的所有連接組成的隊列),否則處于阻塞狀態(blocking)。
參數:
sockfd依然為socket函數返回的文件描述符;
blocklog為設定的監聽隊列的長度。可設為5、10等值但是不能大于SOMAXCONN(監聽隊列最大長度)
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

說說監聽隊列(如下圖所示):?
這里寫圖片描述
監聽隊列包括請求連接建立過程中的兩個子隊列:未完成連接的隊列和已完成連接的隊列。區分的標志就是:是否完成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結構體地址以及結構體變量長度
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(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緩沖區中。
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(6)、close:

int close(int fd);
/*
關閉套接字,類似于fclose,fd為要關閉的套接字文件描述符
失敗返回-1,成功返回0
*/
  • 1
  • 2
  • 3
  • 4
  • 5

3、其它函數:

(1)、字節序轉換函數:

/*由于我們一般普遍所用的機器(x86)都是小端存儲模式或者說叫做小端字節序,而網絡傳輸中采用的是大端字節序,所以要進行網絡通訊,就必須將進行字節序的轉換,之后才可以進行正常信息傳遞。*/uint32_t htonl(uint32_t hostlong);/*主機字節序轉換成網絡字節序*/
uint32_t ntohl(uint32_t netlong);/*網絡字節序轉換成主機字節序*/
  • 1
  • 2
  • 3
  • 4

(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
  • 2
  • 3
  • 4

三、代碼實現:

(1)、服務器(Server):服務器由于不知道客戶何時回請求建立連接,所以必須綁定端口之后進行監聽(Socket、Bind、Listen)?
(2)、客戶端(Client):客戶端只需要向服務器發起請求連接(connect),而不需要綁定與監聽的步驟;?
(3)、請求連接由客戶端發起(主動打開),服務器接受連接請求(被動打開),會經過TCP三次握手過程;而斷開連接服務器和客戶端都可以自行斷開,會經過TCP四次揮手的過程。

1、服務器代碼(Server):

# 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;
}
  • 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
  • 66

2、客戶端代碼(Client):

# 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 1024int 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
  • 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

3、程序測試(Test):

測試中把文件描述輸出了一下,可以觀察到每個進程有屬于自己的一套描述符,而且都是從3開始。因為0,1,2已經被標準輸入輸出一標準錯誤占用了。

1.Client to quit at first:?
這里寫圖片描述

2.Server to quit at first:?
這里寫圖片描述


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/384402.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/384402.shtml
英文地址,請注明出處:http://en.pswp.cn/news/384402.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux系統編程(八)線程

Linux系統編程&#xff08;八&#xff09;線程一、什么是線程&#xff1f;二、Linux內核線程實現原理線程共享資源線程非共享資源線程優缺點線程控制原語一、什么是線程&#xff1f; LWP&#xff1a;light weight process 輕量級的進程&#xff0c;本質仍是進程(在Linux環境下…

智能算法(GA、DBO等)求解阻塞流水車間調度問題(BFSP)

先做一個聲明&#xff1a;文章是由我的個人公眾號中的推送直接復制粘貼而來&#xff0c;因此對智能優化算法感興趣的朋友&#xff0c;可關注我的個人公眾號&#xff1a;啟發式算法討論。我會不定期在公眾號里分享不同的智能優化算法&#xff0c;經典的&#xff0c;或者是近幾年…

Linux socket編程,對套接字進行封裝

轉自&#xff1a;http://www.cnblogs.com/-Lei/archive/2012/09/04/2670942.html 下面是對socket操作的封裝&#xff0c;因為在Linux下寫中文到了windows里面會亂碼&#xff0c;所以注釋用英文來寫&#xff0c;有空再查下解決方法吧 socket.h #ifndef SOCKET_H #define SOCKET_…

Linux系統編程(九)線程同步

Linux系統編程&#xff08;九&#xff09;線程同步一、什么是線程同步&#xff1f;二、互斥量三、條件變量pthread_cond_wait函數pthread_cond_signal函數生產者和消費者模型一、什么是線程同步&#xff1f; 線程同步&#xff0c;指一個線程發出某一功能調用時&#xff0c;在沒…

linux網絡編程(一)網絡基礎傳輸知識

linux網絡編程&#xff08;一&#xff09;網絡傳輸基礎知識一、什么是協議&#xff1f;二、使用步驟典型協議2.網絡應用程序設計模式C/S模式B/S模式優缺點3.分層模型4.TCP/IP四層模型通信過程5.協議格式數據包封裝以太網幀格式ARP數據報格式IP段格式UDP數據報格式TCP數據報格式…

linux網絡編程:使用多進程實現socket同時收發數據

轉載&#xff1a;http://blog.csdn.net/li_wen01/article/details/52685844 前面已講過使用一個進程實現服務端和客戶端P2P通信的實例&#xff0c;但是它只能同時處理一個客戶端的連接。如果要實現并發處理多個客戶端的連接并且實現P2P通信&#xff0c;可以使用多進程來處理。相…

Linux 進程學習(四)------ sigaction 函數

轉自&#xff1a;http://www.cnblogs.com/wblyuyang/archive/2012/11/13/2768923.html 使用 sigaction 函數&#xff1a; signal 函數的使用方法簡單&#xff0c;但并不屬于 POSIX 標準&#xff0c;在各類 UNIX 平臺上的實現不盡相同&#xff0c;因此其用途受 到了一定的限制…

linux網絡編程(二)高并發服務器

linux網絡編程&#xff08;二&#xff09;高并發服務器錯誤處理高并發服務器多進程并發服務器客戶端錯誤處理 #include "wrap.h"int Bind(int fd, const struct sockaddr* sa, socklen_t salen) {int ret;if ((ret bind(fd, sa, salen)) < 0){perror("bind…

linux知識(一) 程序、進程與線程

linux知識&#xff08;一&#xff09; 程序、進程與線程程序進程程序如何變成進程&#xff1f;線程線程與進程fork和創建新線程的區別優點程序 程序&#xff1a;程序是已編譯好的二進制文件&#xff0c;存儲在磁盤中&#xff0c;不占用系統資源 程序包括&#xff1a; RO段&am…

linux 信號signal和sigaction理解

轉載&#xff1a;http://blog.csdn.net/beginning1126/article/details/8680757 今天看到unp時發現之前對signal到理解實在淺顯&#xff0c;今天拿來單獨學習討論下。 signal&#xff0c;此函數相對簡單一些&#xff0c;給定一個信號&#xff0c;給出信號處理函數則可&#xff…

linux知識(二)互斥量、信號量和生產者消費者模型

linux知識&#xff08;二&#xff09;互斥量、信號量和生產者消費者模型一、互斥量產生原因二、信號量生產者消費者模型一、互斥量 產生原因 使用多線程常常會碰到數據混亂的問題&#xff0c;那么使用互斥量&#xff0c;相當于“加鎖”的操作&#xff0c;將有助于解決數據混亂…

基于Linux的Socket編程之TCP全雙工Server-Client聊天程序

轉載&#xff1a;http://blog.csdn.net/apollon_krj/article/details/53437764#0-tsina-1-58570-397232819ff9a47a7b7e80a40613cfe1 一、引言&#xff1a; 由于accept函數、read、write、recv、send等函數都是是阻塞式的&#xff0c;在同一個進程之中&#xff0c;只要有任何一個…

數據結構(一)線性表

數據結構&#xff08;一&#xff09;線性表一、線性表定義二、順序表定義動態數組三、單鏈表定義不帶頭結點帶頭結點頭結點與不帶頭結點的區別頭插法與尾插法雙鏈表循環鏈表循環單鏈表循環雙鏈表靜態鏈表一、線性表定義 線性表是具有相同數據類型的n個數據元素的有限序列 特點…

linux網絡編程(二)TCP通訊狀態

linux網絡編程&#xff08;二&#xff09;TCP通訊狀態TCP狀態轉換為什么需要等待2MSL&#xff1f;端口復用TCP狀態轉換 tcp協議連接開始會經過三次握手&#xff0c;客戶端和服務器開始都會處于CLOSED狀態 第一次握手&#xff1a;客戶端會先發送SYN請求給服務器&#xff0c;客戶…

gethostbyname() 函數說明

轉載&#xff1a;http://www.cnblogs.com/cxz2009/archive/2010/11/19/1881611.html gethostbyname()函數說明——用域名或主機名獲取IP地址 包含頭文件 #include <netdb.h> #include <sys/socket.h> 函數原型 struct hostent *gethostbyna…

linux網絡編程(三)select、poll和epoll

linux網絡編程&#xff08;三&#xff09;select、poll和epoll一、為什么會有多路I/O轉接服務器&#xff1f;二、select三、poll三、epoll一、為什么會有多路I/O轉接服務器&#xff1f; 為什么會有多路I/O轉接服務器呢&#xff1f;在學這個之前&#xff0c;我們同使用的是多線…

Linux socket編程(一) 對套接字操作的封裝

轉載:http://www.cnblogs.com/-Lei/archive/2012/09/04/2670942.html 以前寫的&#xff0c;現在回顧一下&#xff1a; 下面是對socket操作的封裝&#xff0c;因為在Linux下寫中文到了windows里面會亂碼&#xff0c;所以注釋用英文來寫&#xff0c;有空再查下解決方法吧 socket.…

數據結構(二)棧

棧一、棧順序棧線性棧(不帶頭結點)線性棧(帶頭結點)共享棧一、棧 棧是只允許在一端進行插入或刪除操作的線性表。 棧頂&#xff1a;線性表允許進行插入刪除的那一端 棧底&#xff1a;固定的&#xff0c;不允許進行插入和刪除的那一端 空棧&#xff1a;不含如何元素的空表 順序…

如何在linux上安裝sqlite數據庫

如何在linux上安裝sqlite數據庫一、下載二、解壓三、配置&#xff08;configure&#xff09;四、編譯和安裝五、執行sqlite3程序六、測試代碼一、下載 首先要先下載sqlite3源碼包 鏈接&#xff1a;https://pan.baidu.com/s/1_70342ZLlPjLlqGzpy5IHw 提取碼&#xff1a;84ne …

數據結構(三)隊列

數據結構&#xff08;三&#xff09;隊列隊列隊列&#xff08;順序存儲&#xff09;循環隊列&#xff08;順序存儲&#xff09;隊列&#xff08;鏈式存儲&#xff09;隊列 隊列是一種受限制的線性表&#xff0c;只允許表的一端插入&#xff0c;在表的另一端刪除 隊列&#xf…