0

Bài 4: Hello World! Xây dựng ứng dụng Chat Client-Server đầu tiên (TCP)

Ở Bài 3, chúng ta đã mổ xẻ vòng đời lý thuyết của một kết nối Socket thông qua các system calls (socket, bind, listen, accept, connect). Bây giờ là lúc biến lý thuyết thành hiện thực!

Trong bài học này, chúng ta sẽ bước sang Phần 2 (Thực chiến) và tự tay xây dựng một ứng dụng Chat Client-Server đơn giản sử dụng giao thức TCP bằng ngôn ngữ Golang — ngôn ngữ cực kỳ mạnh mẽ trong lập trình hệ thống mạng.

1. Kiến trúc ứng dụng Chat 1-1

Mô hình ứng dụng của chúng ta gồm hai chương trình độc lập:

  1. TCP Server: Mở cổng (Port 8080) chờ kết nối. Khi có client kết nối đến, server sẽ nhận tin nhắn từ client, in ra màn hình và gửi câu trả lời ngược lại (Echo / Chat phản hồi).

  2. TCP Client: Chủ động kết nối tới Server, cho phép người dùng gõ phím từ bàn phím terminal để gửi tin nhắn đi và nhận phản hồi về.

2. Hiện thực hóa TCP Server

Tạo file server.go. Server sẽ liên tục lắng nghe và chấp nhận các kết nối mới, sau đó xử lý đọc/ghi dữ liệu qua vòng lặp:

Go

package main

import (
	"bufio"
	"fmt"
	"net"
	"strings"
)

func main() {
	// 1. Bind và Listen trên cổng 8080 (tương đương socket -> bind -> listen)
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Printf("Lỗi khởi động server: %v\n", err)
		return
	}
	defer listener.Close()

	fmt.Println("🚀 TCP Server đang chạy và lắng nghe tại cổng :8080...")

	for {
		// 2. Accept kết nối từ Client tới (Chặn cho đến khi có client gọi connect)
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
			continue
		}

		// Xử lý client vừa kết nối
		fmt.Printf("🔗 Đã kết nối thành công với Client: %s\n", conn.RemoteAddr().String())
		go handleClient(conn) // Dùng goroutine để xử lý độc lập
	}
}

func handleClient(conn net.Conn) {
	defer conn.Close()

	// Đọc dữ liệu gửi đến từ client theo dòng (line-by-line)
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		message := scanner.Text()
		fmt.Printf("💬 Nhận từ client: %s\n", message)

		// Nếu client gửi "exit", ngắt kết nối
		if strings.TrimSpace(message) == "exit" {
			fmt.Println("👋 Client đã ngắt kết nối.")
			conn.Write([]byte("Goodbye!\n"))
			break
		}

		// Gửi phản hồi ngược lại cho client (Echo server)
		reply := fmt.Sprintf("Server đã nhận: %s\n", message)
		conn.Write([]byte(reply))
	}
}

3. Hiện thực hóa TCP Client

Tạo file client.go. Client sẽ chủ động kết nối tới địa chỉ localhost:8080 và mở luồng đọc/ghi với bàn phím:

Go

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	// 1. Chủ động kết nối tới Server (tương đương socket -> connect)
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Printf("Không thể kết nối đến server: %v\n", err)
		return
	}
	defer conn.Close()

	fmt.Println("✅ Đã kết nối tới TCP Server! Hãy nhập tin nhắn (gõ 'exit' để thoát):")

	// Luồng phụ: Lắng nghe phản hồi từ Server trả về
	go func() {
		scanner := bufio.NewScanner(conn)
		for scanner.Scan() {
			fmt.Printf("\n[Server phản hồi]: %s\n> ", scanner.Text())
		}
	}()

	// Luồng chính: Đọc dữ liệu từ bàn phím và gửi đi
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("> ")
		text, _ := reader.ReadString('\n')

		// Gửi dữ liệu qua socket connection
		_, err := conn.Write([]byte(text))
		if err != nil {
			fmt.Println("Mất kết nối với server.")
			break
		}

		if strings.TrimSpace(text) == "exit" {
			break
		}
	}
}

4. Chạy thử nghiệm ứng dụng (Testing)

Để kiểm chứng ứng dụng chat TCP vừa viết, bạn mở hai cửa sổ Terminal độc lập trên máy tính:

  1. Cửa sổ 1 (Chạy Server):

    Bash

    go run server.go
    
    

    (Màn hình sẽ hiển thị: 🚀 TCP Server đang chạy và lắng nghe tại cổng :8080...)

  2. Cửa sổ 2 (Chạy Client):

    Bash

    go run client.go
    
    

    (Màn hình sẽ hiển thị: ✅ Đã kết nối tới TCP Server!...)

  3. Thực hiện nhắn tin: Gõ thử một vài câu bất kỳ ở phía Client (Cửa sổ 2) và quan sát Server (Cửa sổ 1) lập tức nhận được dữ liệu, xử lý và gửi phản hồi ngược lại!

Tổng kết

Chúc mừng bạn! Bạn vừa tự tay viết xong ứng dụng mạng Client-Server đầu tiên sử dụng giao thức TCP, hiện thực hóa toàn bộ các bước listen, accept, connect và truyền nhận dữ liệu qua luồng read/write.

Tuy nhiên, ứng dụng hiện tại mới chỉ xử lý được chuỗi văn bản đơn giản. Chuyện gì sẽ xảy ra nếu chúng ta muốn truyền tải những gói dữ liệu lớn hơn nhiều, ví dụ như gửi một file hình ảnh hoặc file âm thanh qua mạng? Làm thế nào để chia nhỏ gói tin (chunk) và ráp lại chính xác ở đầu nhận?

Đó chính là nội dung của Bài 5 tiếp theo.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí