服務端和客戶端通過Socket通信過程
參考博客:
TCP網絡編程中connect()、listen()和accept()三者之間的關係
網絡套接字編程基本api
服務端:
- socket() :創建套接字,設置套接字IP地址類型、傳輸協議類型。
- bind():綁定ip地址和端口號到套接字。
- listen():將套接字變成被動的連接監聽套接字。監聽套接字的端口號,隨時準備接收客戶端發來的連接請求。
listen()函數不會阻塞,它主要做的事情爲,將該套接字和套接字對應的連接隊列長度告訴 Linux 內核,然後,listen()函數就結束。 - accept():阻塞直到有客戶端成功連接,並取走隊列中已完成的連接。
accept()函數從處於 established 狀態的連接隊列頭部取出一個已經完成的連接,如果這個隊列沒有已經完成的連接,accept()函數就會阻塞當前線程,直到取出隊列中已完成的客戶端連接爲止。 - read()\write()、recv()\send():與客戶端之間進行數據的交換。
read()是阻塞I/O模式,服務端進程會被阻塞直到在內核緩衝區接收到完整數據,並從內核緩衝區拷貝到進程中爲止。 - close():關閉套接字,即關閉連接。
客戶端:
- socket():創建套接字,設置套接字IP地址類型、傳輸協議類型。
客戶端是發送連接請求一方,端口不用固定,可以隨機分配,因此不用綁定。 - connect():客戶端根據服務器的ip地址和端口號,請求與服務端連接。
客戶端主動連接服務器,建立連接是通過三次握手,而這個連接的過程是由內核完成,不是這個函數完成的,這個函數的作用僅僅是通知 Linux 內核,讓 Linux 內核自動完成 TCP 三次握手連接,最後把連接的結果返回給這個函數的返回值(成功連接爲0, 失敗爲-1)。 - read()\write()、recv()\send()
- close()
服務端代碼
主要實現功能是將客戶端發送的字符串中的小寫字母全部轉化爲大寫字母,再返回給客戶端。(PS:本人新學,所以代碼註釋的比較詳細,實際上代碼量不多的)參考博客:Linux socket編程
#include <iostream>
#include <stdio.h>
#include <cstring> // void *memset(void *s, int ch, size_t n);
#include <sys/types.h> // 數據類型定義
#include <sys/socket.h> // 提供socket函數及數據結構sockaddr
#include <arpa/inet.h> // 提供IP地址轉換函數,htonl()、htons()...
#include <netinet/in.h> // 定義數據結構sockaddr_in
#include <ctype.h> // 小寫轉大寫
#include <unistd.h> // close()、read()、write()、recv()、send()...
using namespace std;
const int flag = 0; // 0表示讀寫處於阻塞模式
const int port = 8080;
const int buffer_size = 1<<20;
int main(int argc, const char* argv[]){
// 創建服務器監聽的套接字。Linux下socket被處理爲一種特殊的文件,返回一個文件描述符。
// int socket(int domain, int type, int protocol);
// domain設置爲AF_INET/PF_INET,即表示使用ipv4地址(32位)和端口號(16位)的組合。
int server_sockfd = socket(PF_INET,SOCK_STREAM,0);
if(server_sockfd == -1){
close(server_sockfd);
perror("socket error!");
}
// /* Enable address reuse */
// int on = 1;
// int ret = setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
// 此數據結構用做bind、connect、recvfrom、sendto等函數的參數,指明地址信息。
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr)); // 結構體清零
server_addr.sin_family = AF_INET; // 協議
server_addr.sin_port = htons(port); // 端口16位, 此處不用htons()或者錯用成htonl()會連接拒絕!!
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地所有IP
// 另一種寫法, 假如是127.0.0.1
// inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr.s_addr);
// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// bind()函數的主要作用是把ip地址和端口綁定到套接字(描述符)裏面
// struct sockaddr是通用的套接字地址,而struct sockaddr_in則是internet環境下套接字的地址形式,二者長度一樣,都是16個字節。二者是並列結構,指向sockaddr_in結構的指針也可以指向sockaddr。
// 一般情況下,需要把sockaddr_in結構強制轉換成sockaddr結構再傳入系統調用函數中。
if(bind(server_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1){
close(server_sockfd);
perror("bind error");
}
// 第二個參數爲相應socket可以排隊的準備道來的最大連接個數
if(listen(server_sockfd, 5) == -1){
close(server_sockfd);
perror("listen error");
}
printf("Listen on port %d\n", port);
while(1){
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// accept()函數從處於established狀態的連接隊列頭部取出一個已經完成的連接,
// 如果這個隊列沒有已經完成的連接,accept()函數就會阻塞當前線程,直到取出隊列中已完成的客戶端連接爲止。
int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
char ipbuf[128];
printf("client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(client_addr.sin_port));
// 實現客戶端發送小寫字符串給服務端,服務端將小寫字符串轉爲大寫返回給客戶端
char buf[buffer_size];
while(1) {
// read data, 阻塞讀取
int len = recv(client_sockfd, buf, sizeof(buf),flag);
if (len == -1) {
close(client_sockfd);
close(server_sockfd);
perror("read error");
}else if(len == 0){ // 這裏以len爲0表示當前處理請求的客戶端斷開連接
break;
}
printf("read buf = %s", buf);
// 小寫轉大寫
for(int i=0; i<len; ++i) {
buf[i] = toupper(buf[i]);
}
printf("after buf = %s", buf);
// 大寫串發給客戶端
if(send(client_sockfd, buf, strlen(buf),flag) == -1){
close(client_sockfd);
close(server_sockfd);
perror("write error");
}
memset(buf,'\0',len); // 清空buf
}
close(client_sockfd);
}
close(server_sockfd);
return 0;
}
客戶端代碼
// client 端相對簡單, 另外可以使用nc命令連接->nc ip prot
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
const int port = 8080;
const int buffer_size = 1<<20;
int main(int argc, const char *argv[]) {
int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (client_sockfd == -1) {
perror("socket error");
exit(-1);
}
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(port);
inet_pton(AF_INET, "127.0.0.1", &client_addr.sin_addr.s_addr);
int ret = connect(client_sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));
if (ret == -1) {
perror("connect error");
exit(-1);
}
while(1) {
char buf[buffer_size] = {0};
fgets(buf, sizeof(buf), stdin); // 從終端讀取字符串
write(client_sockfd, buf, strlen(buf));
//接收, 阻塞等待
int len = read(client_sockfd, buf, sizeof(buf));
if (len == -1) {
perror("read error");
exit(-1);
}
printf("client recv %s\n", buf);
}
close(client_sockfd);
return 0;
}
遇到問題
connect error: Connection refused
,客戶端連接服務器的時候,連接被拒絕。
原因:由於主機字節序和網絡字節序轉換的函數錯誤使用,對端口的轉換用了htonl()
。端口是16位的,應該使用htons()、ntohs();IP是32位的,應該使用htonl()、ntohl()。
(注:數據流有大端字節序和小端字節序之分,TCP/IP協議規定網絡數據流採用大端字節序。通過對大小端的存儲原理分析可發現,對於char型數據,由於其只佔一個字節,所以不存在這個問題,這也是一般情況下把數據緩衝區定義成char類型的原因之一。對於 IP 地址、端口號等非char型數據,必須在數據發送到網絡上之前將其轉換成大端模式,在接收到數據之後再將其轉換成符合接收端主機的存儲模式。)
// Linux 系統爲主機字節序和網絡字節序的轉換提供了4個函數
#include <arpa/inet.h>
/*主機字節順序 --> 網絡字節順序*/
uint32_t htonl(uint32_t hostlong);/* IP */
uint16_t htons(uint16_t hostshort);/* 端口 */
/*網絡字節順序 --> 主機字節順序*/
uint32_t ntohl(uint32_t netlong);/* IP */
uint16_t ntohs(uint16_t netshort);/* 端口 */
bind error: Address already in use
,綁定的地址(ip+端口)已經在使用中。
原因:Ctrl+Z中斷任務的執行,但該任務並沒結束,它只是在進程中維持掛起的狀態。(Ctrl+C是強制終止程序的執行並結束進程)
用netstat -anp | grep 8080
查看服務器監聽端口8080的使用狀態。發現pid爲4600的服務器進程和4623的客戶端進程還處於ESTABLISHED
狀態,並未結束。用kill -9 pid
結束客戶端和服務端的進程,釋放端口。