TCP/IP(7)-TCP Server與TCP Client(linux套接字)
前面幾篇文章談到的關於TCP/IP應用層以下的協議,這些協議最終是在作業系統核心中實現的,套接字API是unix系統用於網路連線的介面,後來被移植到windows系統中,就有了winsock。
TCP的Client/Server模式
在TCP/IP協議中已經講解了TCP協議中三次握手和四次握手過程,以及傳送訊息和接受訊息。那麼在linux系統中,核心中已經將這些協議實現,現在我們一起看看linux下套接字程式設計的API。
TCP伺服器端
1. 建立套接字
#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回:非負描述字---成功 -1---失敗
第一個引數指明瞭協議簇,目前支援5種協議簇,最常用的有AF_INET(IPv4協議)和AF_INET6(IPv6協議);第二個引數指明套介面型別,有三種類型可選:SOCK_STREAM(位元組流套介面)、SOCK_DGRAM(資料報套介面)和SOCK_RAW(原始套介面);如果套介面型別不是原始套介面,那麼第三個引數就為0。
2.繫結套接字
把一個套接字地址(本機IP和埠號)繫結到建立的套接字上。繫結套接字時可以選擇指定IP地址和埠,也可以不指定。通配的IP地址用INADDR_ANY表示,通配的埠用0表示,通配的情況下由核心為其指定相應的IP地址和埠號。
對於客戶端可以繫結套接字,但是一般不需要,因為客戶端的埠號只是臨時的,由核心來分配更合理。但是對伺服器而言,一般要使用知名埠號,如果不進行繫結,客戶端不知道目的埠號,連線不能完成。
通配地址實現:htonl(INADDR_ANY)
通配地址,核心將等到套接字已連線TCP或已經發出資料報(UDP)時才指定。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
返回:0---成功 -1---失敗
3.監聽
socket建立的套接字是主動套接字,呼叫listen後變成監聽套接字。TCP狀態有CLOSE躍遷到LISTEN狀態。
backlog是已完成佇列和未完成佇列大小之和,對於監聽套接字有兩個佇列,一個是未完成佇列,一個是已完成佇列。
- 未完成佇列:客戶端傳送一個SYN包,伺服器收到後變成SYN_RCVD狀態,這樣的套接字被加入到未完成佇列中。
- 已完成佇列:TCP已經完成了3次握手後,將這個套接字加入到已完成佇列,套接字處於ESTABLISHED狀態。
下圖中可以看出,TCP的三次握手是在呼叫connect函式時完成的,伺服器端沒有呼叫函式,但是必須有套接字在某個埠監聽,不然會返回客戶端RST,終止連線。
#include<sys/socket.h>
int listen(int sockfd, int backlog);
呼叫listen函式後的套接字稱為監聽套接字。
4.accept函式
accept函式從已完成連線的佇列中取走一個套接字,如果該佇列為空,則accept函式阻塞。accept函式的返回值稱為已連線套接字,已連線的套接字就建立一個完整的TCP連線,源IP地址,源埠號,目的IP地址,目的埠號都是唯一確定了。
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);
5.資料傳輸
- write和read函式:當伺服器和客戶端的連線建立起來後,就可以進行資料傳輸了,伺服器和客戶端用各自的套接字描述符進行讀/寫操作。因為套接字描述符也是一種檔案描述符,所以可以用檔案讀/寫函式write()和read()進行接收和傳送操作。
write()函式用於資料的傳送
#include <unistd.h>
int write(int sockfd, char *buf, int len);
回:非負---成功 -1---失敗
引數sockfd是套接字描述符,對於伺服器是accept()函式返回的已連線套接字描述符,對於客戶端是呼叫socket()函式返回的套接字描述符;引數buf是指向一個用於傳送資訊的資料緩衝區;len指明傳送資料緩衝區的大小。
read()函式用於資料的接收
#include <unistd.h>
int read(int sockfd, char *buf, intlen);
回:非負---成功 -1---失敗
引數sockfd是套接字描述符,對於伺服器是accept()函式返回的已連線套接字描述符,對於客戶端是呼叫socket()函式返回的套接字描述符;引數buf是指向一個用於接收資訊的資料緩衝區;len指明接收資料緩衝區的大小。
- send和recv函式:TCP套接字提供了send()和recv()函式,用來發送和接收操作。這兩個函式與write()和read()函式很相似,只是多了一個附加的引數。
(1)send()函式用於資料的傳送。
#include <sys/types.h>
#include < sys/socket.h >
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
回:返回寫出的位元組數---成功 -1---失敗
前3個引數與write()相同,引數flags是傳輸控制標誌。
(2)recv()函式用於資料的傳送。
#include <sys/types.h>
#include < sys/socket.h >
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
回:返回讀入的位元組數---成功 -1---失敗
前3個引數與read()相同,引數flags是傳輸控制標誌。
6.關閉套接字
close函式關閉套接字
#include <unistd.h>
int close(int sockfd);
TCP客戶端
1.建立套接字
2.連線伺服器
TCP用connect函式來建立與TCP伺服器的連線。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
返回:0---成功 -1---失敗
客戶端傳送的SYN包可能會遇到失敗,可能有以下幾種情況:
1. 如果客戶端沒有收到SYN的響應包,根據TCP的超時重發機制進行重發。75秒後還沒收到,就返回錯誤。
2. 如果目的主機沒有監聽目的埠號,就會返回一個RST的分節,客戶端收到RST後立刻返回錯誤。
3. 如果SYN在中間路由遇到目的不可達,客戶端收到ICMP報文,客戶端儲存這個報文資訊,並採用第一種情況方案解決,也就是重發。
3.收發資料
4.關閉套接字
TCP聊天室服務端程式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFLEN 100
const char* IP = "127.0.0.1";
const unsigned int SERV_PORT = 7777;
void Chat(int sockfd);
int main(int argc, char *argv[])
{
int listenfd, connectfd;
struct sockaddr_in s_addr, c_addr;
char buf[BUFLEN];
unsigned int port, listnum;
pid_t childpid;
socklen_t len;
/*建立socket*/
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
/*設定伺服器埠*/
port = SERV_PORT;
/*設定偵聽佇列長度*/
listnum = 5;
/*設定伺服器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
// s_addr.sin_addr.s_addr = inet_aton(IP, &s_addr.sin_addr);
s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/*把地址和埠幫定到套接字上*/
if((bind(listenfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){
perror("bind");
exit(errno);
}
/*偵聽本地埠*/
if(listen(listenfd, listnum) == -1){
perror("listen");
exit(errno);
}
while(1){
printf("*****************server start***************\n");
len = sizeof(struct sockaddr);
if((connectfd = accept(listenfd, (struct sockaddr*) &c_addr, &len)) == -1){
perror("accept");
exit(errno);
}
else
{
printf("connected with client, IP is: %s, PORT is: %d\n", inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port));
}
//建立子程序
if((childpid = fork()) == 0)
{
Chat(connectfd);
/*關閉已連線套接字*/
close(connectfd);
/*是否退出伺服器*/
printf("exit?:y->yes;n->no ");
bzero(buf, BUFLEN);
fgets(buf,BUFLEN, stdin);
if(!strncasecmp(buf,"y",1)){
printf("server stop\n");
break;
}
//退出子程序
exit(0);
}
}
/*關閉監聽的套接字*/
close(listenfd);
return 0;
}
void Chat(int sockfd)
{
socklen_t len;
char buf[BUFLEN];
while(1)
{
_retry:
/******傳送訊息*******/
bzero(buf,BUFLEN);
printf("enter your words:");
/*fgets函式:從流中讀取BUFLEN-1個字元*/
fgets(buf,BUFLEN,stdin);
/*打印發送的訊息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4))
{
printf("server stop\n");
break;
}
/*如果輸入的字串只有"\n",即回車,那麼請重新輸入*/
if(!strncmp(buf,"\n",1))
{
goto _retry;
}
/*如果buf中含有'\n',那麼要用strlen(buf)-1,去掉'\n'*/
if(strchr(buf,'\n'))
{
len = send(sockfd, buf,strlen(buf)-1,0);
}
/*如果buf中沒有'\n',則用buf的真正長度strlen(buf)*/
else
{
len = send(sockfd,buf,strlen(buf),0);
}
if(len > 0)
printf("send successful\n");
else{
printf("send failed\n");
break;
}
/******接收訊息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("receive massage:%s\n",buf);
else
{
if(len < 0 )
printf("receive failed\n");
else//伺服器呼叫close函式後,系統阻塞函式呼叫,返回0
printf("client stop\n");
break;
}
}
}
TCP聊天室客戶端程式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFLEN 100
const char* IP = "127.0.0.1";
const int SERV_PORT = 7777;
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in s_addr;
socklen_t len;
unsigned int port;
char buf[BUFLEN];
/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
/*設定伺服器埠*/
port = SERV_PORT;
/*設定伺服器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
if(inet_aton(IP, (struct in_addr*)&s_addr.sin_addr.s_addr) == 0){
perror("IP error");
exit(errno);
}
/*開始連線伺服器*/
if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(errno);
}else
printf("*****************client start***************\n");
while(1){
/******接收訊息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("receive massage:%s\n",buf);
else{
if(len < 0 )
printf("receive failed\n");
else
printf("server stop\n");
break;
}
_retry:
/******傳送訊息*******/
bzero(buf,BUFLEN);
printf("enter your words:");
/*fgets函式:從流中讀取BUFLEN-1個字元*/
fgets(buf,BUFLEN,stdin);
/*打印發送的訊息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("client stop\n");
break;
}
/*如果輸入的字串只有"\n",即回車,那麼請重新輸入*/
if(!strncmp(buf,"\n",1)){
goto _retry;
}
/*如果buf中含有'\n',那麼要用strlen(buf)-1,去掉'\n'*/
if(strchr(buf,'\n'))
len = send(sockfd,buf,strlen(buf)-1,0);
/*如果buf中沒有'\n',則用buf的真正長度strlen(buf)*/
else
len = send(sockfd,buf,strlen(buf),0);
if(len > 0)
printf("send successful\n");
else{
printf("send failed\n");
break;
}
}
/*關閉連線*/
close(sockfd);
return 0;
}