Chúng ta đã làm quen với giao thức TCP Client chế độ Server, với TCP Client, mọi thứ khó hơn rất nhiều, cũng giống như các bài trước, mình sẽ làm với từng phần nhỏ một. Ở bài này chúng ta sẽ thử cho ENC28J60 kết nối với một TCP trong mạng LAN nhé !
Chú ý: Mình đã chuyển project sang chíp stm32f103c8t6, các bạn phải đọc bài 15 trước nhé
http://iot47.com/enc28j60-bai-15-giao-thuc-dhcp-lay-ip-dong/
Các bước thiết lập kết nối
- Client gửi bản tin SYN với 1 Sequence_Number bất kì
- Server sẽ trả về bản tin SYN|ACK với Acknowledgement = Sequence_Number +1
- Client trả lại với bản tin ACK
Hoàn tất kết nối. Sau khi kết nối thành công, chúng ta sẽ không cho Client kết nối lại nữa nhé vì điều này là không cần thiết
Ở trong file tcp.h chúng ta đĩnh nghĩa thêm 1 struct để lưu các thông tin kết nối với Server
1 2 3 4 5 6 7 8 9 10 |
typedef struct { uint32_t Sequence_Number; uint32_t Acknowledgement; uint8_t tcp_status; uint16_t port; uint16_t client_port; uint8_t mac_defaul[6]; uint8_t ip_defaul[4]; }__attribute__ ((packed)) TCP_client; |
Khởi tạo Struct TCP_client vào đầu file tcp.c nhé
1 |
TCP_client tcp_client1; |
Biến tcp_status
tcp_status = 1 => đã gửi gói tin SYN, đang chờ SYN|ACK
tcp_status = 2 =>đã nhận được SYN|ACK, đã phản hồi ACK, kết nối thành công
Trong hàm TCP_make_herder chúng ta thêm đoạn code make gói tin connect, gói tin connect sẽ mặc định là có 66 byte data
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 |
memcpy(TCP_Struct_Frame->MAC_dich,tcp_client1.mac_defaul,6); memcpy(TCP_Struct_Frame->MAC_nguon,macaddr,6); TCP_Struct_Frame->Ethernet_type = 0x0008; TCP_Struct_Frame->Header_length = 0x45; //make IP TCP_Struct_Frame->Services=0x0000; TCP_Struct_Frame->TotoLength = swap16(52); //do dai cua goi tin IP mac dinh la 0x0034 (52 byte) (66-14) TCP_Struct_Frame->Identification=0xBF2B; TCP_Struct_Frame->Flag=0x0040; TCP_Struct_Frame->TimeToLive=0x80; TCP_Struct_Frame->Protocol=0x06; //tcp TCP_Struct_Frame->CheckSum=0x0000; memcpy(TCP_Struct_Frame->SourceIP,ip,4); tcp_client1.client_port = 3456; TCP_Struct_Frame->Source_Port = swap16(tcp_client1.client_port); TCP_Struct_Frame->Sequence_Number=swap32(tcp_client1.Sequence_Number); TCP_Struct_Frame->Acknowledgement=0; TCP_Struct_Frame->data_offset=0x80; TCP_Struct_Frame->TCP_Flags = TCP_SYN; TCP_Struct_Frame->Window = swap16(8192); // thong bao cho server biet bo dem nhan toi da TCP_Struct_Frame->TCP_Checksums=0x0000; TCP_Struct_Frame->Urgent_Pointer=0x0000; //option TCP_Struct_Frame->data[0]=0x02; TCP_Struct_Frame->data[1]=0x04; TCP_Struct_Frame->data[2]=0x05; TCP_Struct_Frame->data[3]=0xb4; TCP_Struct_Frame->data[4]=0x01; TCP_Struct_Frame->data[5]=0x03; TCP_Struct_Frame->data[6]=0x03; TCP_Struct_Frame->data[7]=0x08; TCP_Struct_Frame->data[8]=0x01; TCP_Struct_Frame->data[9]=0x01; TCP_Struct_Frame->data[10]=0x04; TCP_Struct_Frame->data[11]=0x02; |
Và định nghĩa FOR_CONNECT có giá trị là 5 vào file tcp.h
1 |
#define FOR_ACK 5 |
Viết hàm gửi gói tin SYN
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 |
uint8_t TCP_sendSYN(uint8_t *IP_server,uint16_t port,uint32_t timeout) { TCP_struct *TCP_Struct_Frame = (TCP_struct *)eth_buffer; uint32_t count=0; if(IP_server[0] == 192 && IP_server[1] == 168) //local ip { while(1) { if(ARP_table_checkIP(IP_server) != -1) { ARP_table_get_MAC(IP_server,tcp_client1.mac_defaul); break; // da nhan dc MAC } ARP_send_request(IP_server); HAL_Delay(50); count+=50; if(count >= timeout) { return 0; //gui that bai } } } tcp_client1.tcp_status = 1; tcp_client1.port = port; memcpy(tcp_client1.ip_defaul,IP_server,4); //save net_readSetStatus(0); //chinh sua thong tin nguoi nhan TCP_make_herder(TCP_Struct_Frame,66,FOR_CONNECT); //ban tin connect co do dai mac dinh la 66 memcpy(TCP_Struct_Frame->DestIP,IP_server,4); TCP_Struct_Frame->Dest_Port = swap16(port); TCP_Struct_Frame->CheckSum = NET_ipchecksum((uint8_t *)&TCP_Struct_Frame->Header_length); //tinh checksum cho goi IP TCP_Struct_Frame->TCP_Checksums = TCP_checksum(TCP_Struct_Frame); sprintf(debug_string,"Connect to %i.%i.%i.%i:%i ...\r\n",IP_server[0],IP_server[1],IP_server[2],IP_server[3],port); UART_putString(debug_string); NET_SendFrame((uint8_t *)TCP_Struct_Frame,66); net_readSetStatus(1); return 1; } |
Để gửi gói tin connect vào 1 thiết bị trong mạng LAN, cần phải có địa chỉ MAC, mình sử dụng hàm ARP_table_get_MAC để lấy
Hàm này trả về 0 nếu không thể lấy MAC thành công, và trả về 1 nếu lấy thành công, đồng thời nó cũng lưu địa chỉ MAC đã lấy được vào struct tcp_client1 luôn
Sau khi gửi xong bản tin SYN, mình sẽ phải chờ bản tin SYN|ACK của server trả về, do phải chờ nên ta sẽ cần 1 cái timeout, nếu quá hạn thì thử gửi lại xem sao.
Hàm TCP_Connect sẽ làm nhiệm vụ trên
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 |
uint8_t TCP_Connect(uint8_t *IP_server,uint16_t port,uint32_t timeout,int8_t Try_reconnect) { uint32_t t; if( tcp_client1.tcp_status==2) { UART_putString("Da ket noi roi !\r\n"); return 2; } tcp_client1.Sequence_Number = rand(); //save Sequence_Number do { if(!TCP_sendSYN(IP_server,80,5000)) // gửi bản tin SYN { UART_putString("Ket noi that bai 1 \r\n"); return 0; //gui that bai } else { t = HAL_GetTick(); while(1) { if(tcp_client1.tcp_status == 2) { UART_putString("Ket noi thanh cong\r\n"); return 1; } if(HAL_GetTick() - t > timeout) break; } } }while(Try_reconnect--); UART_putString("Ket noi that bai 2\r\n"); return 0; //gui that bai } |
Ở đây, mình thêm vào tham số Try_reconnect, tức là số lần cố gắng thử kết nối lại nếu kết nối thất bại. Hàm này sẽ trả về 0 nếu kết nối hoàn toàn thất bại, 1 nếu kết nối thành công, 2 nếu đã kết nối rồi. Không kết nối nữa !
Nhận bản tin SYN|ACK
Trong hàm tcp_read, chúng ta thêm code kiểm tra cờ SYN|ACK
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if(TCP_Struct_Frame->TCP_Flags == (TCP_SYN|TCP_ACK)) { if(tcp_client1.tcp_status == 1 || tcp_client1.tcp_status == 2) { if(swap32(TCP_Struct_Frame->Acknowledgement) == tcp_client1.Sequence_Number+1) { tcp_client1.Sequence_Number+=1; tcp_client1.Acknowledgement = swap32(TCP_Struct_Frame->Sequence_Number) + 1; tcp_client1.tcp_status=2; //ket noi thanh cong TCP_make_herder(TCP_Struct_Frame,len,FOR_ACK); NET_SendFrame((uint8_t *)TCP_Struct_Frame,len); //gui goi tin tcp ack reply UART_putString("Connect OK\r\n"); } } } |
OK. Bây giờ test thử thôi
Khởi tạo server TCP trên máy tính bằng phần mềm Hercules -> TCP Server. Chọn Port 80 -> Listen
Trong file code chính (main.c) mình sẽ add địa chỉ server của máy tính mình đang dùng, nếu các bạn không biết IP của máy tính thì vào cmd gõ ipconfig hoặc tra google với từ khóa “cách kiểm tra ip máy tính” hoặc tham khảo hướng dẫn ở đây
Ví dụ, laptop của mình có ip 192.168.1.18
1 |
uint8_t ip_server[4]={192,168,1,18}; |
Quay trở lại hàm main và gọi TCP_Connect(ip_server,80,5000,5);
Ở đây, tham số 80 chính là port của server mà mình đã lắng nghe ở phần mềm Hercules, 5000 là timeout của mỗi lần connect và 5 là số lần cố gắng thử kết nối lại nếu thất bại
Chạy chương trình và xem màn hình debug
Như vậy mình đã kết nối thành công tới máy chủ do laptop của mình tạo ra. Và trong tab TCP_Server của phần mềm Hercules cũng sẽ có 1 thông báo có 1 client connect tới
Ngắt kết nối
Server có thể chủ động ngắt kết nối bằng bản tin FIN, do vậy chúng ta bắt sự kiện này ở bản tin FIN|ACK đồng thời kiểm tra Acknowledgement xem có trùng với Sequence_Number đã lưu ở trong Struct tcp_client1 không ! Nếu đúng thì mình sẽ reset lại tcp_status
Trong hàm TCP_read tại chỗ kiểm tra cờ TCP_FIN|TCP_ACK, thêm
1 2 3 4 5 6 7 8 9 10 |
if(swap32(TCP_Struct_Frame->Acknowledgement) == tcp_client1.Sequence_Number) { tcp_client1.tcp_status=0; UART_putString("Server da ngat ket noi !\r\n"); } else if(swap32(TCP_Struct_Frame->Acknowledgement) == (tcp_client1.Sequence_Number+1)) { tcp_client1.tcp_status=0; UART_putString("Da ngat ket noi Server!\r\n"); } |
Sau khi kết nối thành công, hãy ấn nút Close trên tab TCP_Server trên tab Hercules, điều này sẽ khiến phần mềm tự động ngắt kết nối với tất cả client
Và đây là toàn bộ quá trình kết nối và ngăt được WireShark ghi lại
Client chủ động ngắt kết nối
Client sẽ chủ động gửi FIN|ACK để ngắt kết nối tới Server
Thêm phần make hearder cho gói disconect trong hàm TCP_make_herder
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 |
else if(type == FOR_DISCONNECT) { memcpy(TCP_Struct_Frame->MAC_dich,tcp_client1.mac_defaul,6); memcpy(TCP_Struct_Frame->MAC_nguon,macaddr,6); TCP_Struct_Frame->Ethernet_type = 0x0008; TCP_Struct_Frame->Header_length = 0x45; //make IP TCP_Struct_Frame->Services=0x0000; TCP_Struct_Frame->TotoLength = swap16(40); //do dai cua goi tin IP mac dinh la 0x0028 (40 byte) ( 54 -14) TCP_Struct_Frame->Identification=0x4470; TCP_Struct_Frame->Flag=0x0040; TCP_Struct_Frame->TimeToLive=0x80; TCP_Struct_Frame->Protocol=0x06; //tcp TCP_Struct_Frame->CheckSum=0x0000; memcpy(TCP_Struct_Frame->SourceIP,ip,4); TCP_Struct_Frame->Source_Port = swap16(tcp_client1.client_port); TCP_Struct_Frame->Sequence_Number=swap32(tcp_client1.Sequence_Number); TCP_Struct_Frame->Acknowledgement=swap32(tcp_client1.Acknowledgement); TCP_Struct_Frame->data_offset=0x50; TCP_Struct_Frame->TCP_Flags = TCP_FIN|TCP_ACK; TCP_Struct_Frame->Window = 0x4400; // thong bao cho server biet bo dem nhan toi da TCP_Struct_Frame->TCP_Checksums=0x0000; TCP_Struct_Frame->Urgent_Pointer=0x0000; } |
Và tạo thêm hàm TCP_Disconect
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 |
uint8_t TCP_Disconect(uint32_t timeout) { uint32_t t; if( tcp_client1.tcp_status==2) { TCP_struct *TCP_Struct_Frame = (TCP_struct *)eth_buffer; net_readSetStatus(0); //chinh sua thong tin nguoi nhan memcpy(TCP_Struct_Frame->DestIP,tcp_client1.ip_defaul,4); TCP_Struct_Frame->Dest_Port = swap16(tcp_client1.port); TCP_make_herder(TCP_Struct_Frame,54,FOR_DISCONNECT); //ban tin disconnect co do dai mac dinh la 54 TCP_Struct_Frame->CheckSum = NET_ipchecksum((uint8_t *)&TCP_Struct_Frame->Header_length); //tinh checksum cho goi IP TCP_Struct_Frame->TCP_Checksums = TCP_checksum(TCP_Struct_Frame); NET_SendFrame((uint8_t *)TCP_Struct_Frame,54); net_readSetStatus(1); t = HAL_GetTick(); while(1) { if( tcp_client1.tcp_status == 0) //ngat ket noi thanh cong { return 1; } if(HAL_GetTick() - t > timeout) { UART_putString("Ngat ket noi that bai\r\n"); return 0; } } } return 2; //chua co ket noi de ngat } |
Ở hàm main, sau khi kết nối thành công mình sẽ thử ngắt kết nối luôn để test hàm ngắt kết nối
1 2 3 4 5 |
if(TCP_Connect(ip_server,80,5000,1)) { HAL_Delay(100); TCP_Disconect(5000); //ngắt kết nối với timeout là 5s } |
Tải FULL Source tại đây
https://drive.google.com/open?id=1LNJPPLRa7aYyXL2uvuLArdp7aOn3KCLA
Lưu ý: Thư viện enc28j60 nằm ở : \code\Drivers\IOT47_lib\ENC28J60
Theo dõi toàn bộ tutorial enc28j60 tại đây
Chào Đào Nguyện
Trước hết cảm ơn bạn vì Series chia sẻ hữu ích
Mình có tải Source bài 16, thay đổi IP bằng IP của máy mình. Bật listen TCP Server trên Hercules. Rồi dùng hàm TCP_Connect() nhưng không kết nối được.
Mình có kiểm tra bằng Wireshark thì IP và MAC của máy tính và ENC đều đúng hết. nhưng k thấy máy tính gửi gói (SYN | ACK).
Nhờ bạn cho mình lời khuyên với. Cảm ơn bạn
Mình connect được rồi, cái Hercules của mình cửa sổ TCP Server. Cài lại hercules là chạy ok. Cảm ơn bạn nhiều :))))
cảm ơn ban đã chia sẻ. mình có 1 vấn đề lỗi thế này: khi mình cho ENC làm client thì nó trao đổi dữ liệu với Server OK. nhưng sau đó mình test với chế độ ngược lại đó là ENC là Server, hercules làm client thì khi connect thì trên hercules báo connected và nên ENC đã lấy dc địa chỉ MAC nhưng không thể trao đổi dữ liệu cho nhau. client dửi dữ liệu cho ENC thì ko thấy ENC in data ra. và cờ PSH|ACK từ clinet được retransmit liên tục đến 5 lần. ENC cũng ko thể gửi dữ liệu lại cho client và nó báo ko có kết nối. bạn cho mình cách FIX nhé, cảm ơn bạn rất nhiều